Merge "Add com.android.virt.accessor_demo" into main
diff --git a/ferrochrome_app/AndroidManifest.xml b/ferrochrome_app/AndroidManifest.xml
index 826fcc4..7afffe5 100644
--- a/ferrochrome_app/AndroidManifest.xml
+++ b/ferrochrome_app/AndroidManifest.xml
@@ -23,6 +23,16 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity android:name=".OpenUrlActivity"
+                  android:theme="@android:style/Theme.NoDisplay"
+                  android:launchMode="singleTask"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.SEND" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="text/*" />
+            </intent-filter>
+        </activity>
     </application>
 
 </manifest>
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
index 5006413..2df5cab 100644
--- a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -55,11 +55,18 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        if (!isTaskRoot()) {
+            // In case we launched this activity multiple times, only start one instance of this
+            // activity by only starting this as the root activity in task.
+            finish();
+            Log.w(TAG, "Not starting because not task root");
+            return;
+        }
         setContentView(R.layout.activity_ferrochrome);
         getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
 
         // Find VM Launcher
-        Intent intent = new Intent(ACTION_VM_LAUNCHER);
+        Intent intent = new Intent(ACTION_VM_LAUNCHER).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
         PackageManager pm = getPackageManager();
         List<ResolveInfo> resolveInfos =
                 pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
new file mode 100644
index 0000000..c32d017
--- /dev/null
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
@@ -0,0 +1,69 @@
+/*
+ * 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.ferrochrome;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+public class OpenUrlActivity extends Activity {
+    private static final String TAG = OpenUrlActivity.class.getSimpleName();
+
+    private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        boolean isRoot = isTaskRoot();
+        finish();
+        if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
+            return;
+        }
+        String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+        if (text == null) {
+            return;
+        }
+        Uri uri = Uri.parse(text);
+        if (uri == null) {
+            return;
+        }
+        String scheme = uri.getScheme();
+        if (!("http".equals(scheme) || "https".equals(scheme) || "mailto".equals(scheme))) {
+            Log.e(TAG, "Unsupported URL scheme: " + scheme);
+            return;
+        }
+        Log.i(TAG, "Sending " + scheme + " URL to VM");
+        if (isRoot) {
+            Log.w(
+                    TAG,
+                    "Cannot open URL without starting "
+                            + FerrochromeActivity.class.getSimpleName()
+                            + " first, starting it now");
+            startActivity(
+                    new Intent(this, FerrochromeActivity.class).setAction(Intent.ACTION_MAIN));
+            return;
+        }
+        startActivity(
+                new Intent(ACTION_VM_OPEN_URL)
+                        .setFlags(
+                                Intent.FLAG_ACTIVITY_SINGLE_TOP
+                                        | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
+                        .putExtra(Intent.EXTRA_TEXT, text));
+    }
+}
diff --git a/ferrochrome_app/vm_config.json.template b/ferrochrome_app/vm_config.json.template
index 6e1992f..d1a7cfa 100644
--- a/ferrochrome_app/vm_config.json.template
+++ b/ferrochrome_app/vm_config.json.template
@@ -33,5 +33,9 @@
         "backend": "virglrenderer",
         "context_types": ["virgl2"]
     },
+    "display": {
+        "scale": "0.77",
+        "refresh_rate": "30"
+     },
     "console_input_device": "hvc0"
 }
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
index 210548a..32db59c 100755
--- a/tests/ferrochrome/ferrochrome.sh
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -32,6 +32,9 @@
 fecr_clean_up() {
   trap - INT
 
+  # Reset screen always on
+  adb shell svc power stayon false
+
   if [[ -d ${fecr_dir} && -z ${fecr_keep} ]]; then
     rm -rf ${fecr_dir}
   fi
@@ -134,33 +137,8 @@
 fi
 
 echo "Ensure screen unlocked"
-
-try_unlock=0
-while [[ "${try_unlock}" -le "${TRY_UNLOCK_MAX}" ]]; do
-  screen_state=$(adb shell dumpsys nfc | sed -n 's/^mScreenState=\(.*\)$/\1/p')
-  case "${screen_state}" in
-    "ON_UNLOCKED")
-      break
-      ;;
-    "ON_LOCKED")
-      # Disclaimer: This can unlock phone only if unlock method is swipe (default after FDR)
-      adb shell input keyevent KEYCODE_MENU
-      ;;
-    "OFF_LOCKED"|"OFF_UNLOCKED")
-      adb shell input keyevent KEYCODE_WAKEUP
-      ;;
-    *)
-      echo "Unknown screen state. Continue to boot, but may fail"
-      break
-      ;;
-  esac
-  sleep 1
-  try_unlock=$((try_unlock+1))
-done
-if [[ "${try_unlock}" -gt "${TRY_UNLOCK_MAX}" ]]; then
-  >&2 echo "Failed to unlock screen. Try again after manual unlock"
-  exit 1
-fi
+adb shell svc power stayon true
+adb shell wm dismiss-keyguard
 
 echo "Starting ferrochrome"
 adb shell am start-activity -a ${ACTION_NAME} > /dev/null
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
index 69527be..af68eb3 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
@@ -33,6 +33,8 @@
 
     private static final String CUTTLEFISH_DEVICE_PREFIX = "vsoc_";
     private static final String CUTTLEFISH_ARM64_DEVICE_PREFIX = "vsoc_arm64";
+    private static final String GOLDFISH_DEVICE_PREFIX = "emu64";
+    private static final String GOLDFISH_ARM64_DEVICE_PREFIX = "emu64a";
     private static final String USER_BUILD_TYPE = "user";
     private static final String HWASAN_SUFFIX = "_hwasan";
 
@@ -65,6 +67,23 @@
     }
 
     /**
+     * @return whether the device is a cuttlefish device.
+     */
+    public boolean isGoldfish() {
+        String vendorDeviceName = getProperty(KEY_VENDOR_DEVICE);
+        return vendorDeviceName != null && vendorDeviceName.startsWith(GOLDFISH_DEVICE_PREFIX);
+    }
+
+    /**
+     * @return whether the device is a cuttlefish device running on 64 bit Arm.
+     */
+    public boolean isGoldfishArm64() {
+        String vendorDeviceName = getProperty(KEY_VENDOR_DEVICE);
+        return vendorDeviceName != null
+                && vendorDeviceName.startsWith(GOLDFISH_ARM64_DEVICE_PREFIX);
+    }
+
+    /**
      * @return whether the build is HWASAN.
      */
     public boolean isHwasan() {
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 91ff4d8..8169376 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -81,6 +81,14 @@
         return getDeviceProperties().isCuttlefishArm64();
     }
 
+    public static boolean isGoldfish() {
+        return getDeviceProperties().isGoldfish();
+    }
+
+    private static boolean isGoldfishArm64() {
+        return getDeviceProperties().isGoldfishArm64();
+    }
+
     public static boolean isHwasan() {
         return getDeviceProperties().isHwasan();
     }
@@ -246,10 +254,11 @@
                 .that(KERNEL_VERSION)
                 .isNotEqualTo("5.4");
 
-        // Cuttlefish on Arm 64 doesn't and cannot support any form of virtualization, so there's
-        // no point running any of these tests.
-        assume().withMessage("Virtualization not supported on Arm64 Cuttlefish. b/341889915")
-                .that(isCuttlefishArm64())
+        // Cuttlefish/Goldfish on Arm 64 doesn't and cannot support any form of virtualization,
+        // so there's no point running any of these tests.
+        assume().withMessage("Virtualization not supported on Arm64 Cuttlefish/Goldfish."
+                + " b/341889915")
+                .that(isCuttlefishArm64() || isGoldfishArm64())
                 .isFalse();
     }
 
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 658b1bb..1465e73 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1219,7 +1219,8 @@
         // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
         // `instance-id` which is input to DICE is contained in DT which is missing in CF.
         assumeFalse(
-                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
+                "Cuttlefish/Goldfish doesn't support device tree under /proc/device-tree",
+                isCuttlefish() || isGoldfish());
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
@@ -1693,7 +1694,8 @@
         // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
         // `instance-id` which is input to DICE is contained in DT which is missing in CF.
         assumeFalse(
-                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
+                "Cuttlefish/Goldfish doesn't support device tree under /proc/device-tree",
+                isCuttlefish() || isGoldfish());
 
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
@@ -2401,7 +2403,8 @@
         assumeSupportedDevice();
         // TODO(b/325094712): Boot fails with vendor partition in Cuttlefish.
         assumeFalse(
-                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
+                "Cuttlefish/Goldfish doesn't support device tree under /proc/device-tree",
+                isCuttlefish() || isGoldfish());
         // TODO(b/317567210): Boot fails with vendor partition in HWASAN enabled microdroid
         // after introducing verification based on DT and fstab in microdroid vendor partition.
         assumeFalse(
diff --git a/vmbase/src/console.rs b/vmbase/src/console.rs
index bbbcb07..cd05250 100644
--- a/vmbase/src/console.rs
+++ b/vmbase/src/console.rs
@@ -15,11 +15,8 @@
 //! Console driver for 8250 UART.
 
 use crate::uart::Uart;
-use core::{
-    cell::OnceCell,
-    fmt::{write, Arguments, Write},
-};
-use spin::mutex::SpinMutex;
+use core::fmt::{write, Arguments, Write};
+use spin::{mutex::SpinMutex, Once};
 
 // Arbitrary limit on the number of consoles that can be registered.
 //
@@ -28,12 +25,8 @@
 
 static CONSOLES: [SpinMutex<Option<Uart>>; MAX_CONSOLES] =
     [SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None)];
-static ADDRESSES: [SpinMutex<OnceCell<usize>>; MAX_CONSOLES] = [
-    SpinMutex::new(OnceCell::new()),
-    SpinMutex::new(OnceCell::new()),
-    SpinMutex::new(OnceCell::new()),
-    SpinMutex::new(OnceCell::new()),
-];
+static ADDRESSES: [Once<usize>; MAX_CONSOLES] =
+    [Once::new(), Once::new(), Once::new(), Once::new()];
 
 /// Index of the console used by default for logging.
 pub const DEFAULT_CONSOLE_INDEX: usize = 0;
@@ -52,7 +45,7 @@
 pub unsafe fn init(base_addresses: &[usize]) {
     for (i, &base_address) in base_addresses.iter().enumerate() {
         // Remember the valid address, for emergency console accesses.
-        ADDRESSES[i].lock().set(base_address).expect("console::init() called more than once");
+        ADDRESSES[i].call_once(|| base_address);
 
         // Initialize the console driver, for normal console accesses.
         let mut console = CONSOLES[i].lock();
@@ -78,8 +71,7 @@
 /// This is intended for use in situations where the UART may be in an unknown state or the global
 /// instance may be locked, such as in an exception handler or panic handler.
 pub fn ewriteln(n: usize, format_args: Arguments) {
-    let Some(cell) = ADDRESSES[n].try_lock() else { return };
-    let Some(addr) = cell.get() else { return };
+    let Some(addr) = ADDRESSES[n].get() else { return };
 
     // SAFETY: addr contains the base of a mapped UART, passed in init().
     let mut uart = unsafe { Uart::new(*addr) };
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index e7a23dc..c6ab1f2 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -17,8 +17,9 @@
                   android:exported="true">
             <intent-filter>
                 <action android:name="android.virtualization.VM_LAUNCHER" />
+                <action android:name="android.virtualization.VM_OPEN_URL" />
                 <category android:name="android.intent.category.DEFAULT" />
-	    </intent-filter>
+            </intent-filter>
         </activity>
     </application>
 
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 694aa57..0b93968 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -17,14 +17,13 @@
 package com.android.virtualization.vmlauncher;
 
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
-import static android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 
 import android.Manifest.permission;
 import android.app.Activity;
 import android.content.ClipData;
 import android.content.ClipboardManager;
+import android.content.Intent;
 import android.crosvm.ICrosvmAndroidDisplayService;
 import android.graphics.PixelFormat;
 import android.graphics.Rect;
@@ -45,7 +44,6 @@
 import android.system.virtualmachine.VirtualMachineManager;
 import android.util.DisplayMetrics;
 import android.util.Log;
-import android.view.Display;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.SurfaceHolder;
@@ -64,6 +62,8 @@
 
 import java.io.BufferedOutputStream;
 import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -81,12 +81,17 @@
 public class MainActivity extends Activity implements InputManager.InputDeviceListener {
     private static final String TAG = "VmLauncherApp";
     private static final String VM_NAME = "my_custom_vm";
+
     private static final boolean DEBUG = true;
+    private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
+
+    private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
+    private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
+
     private ExecutorService mExecutorService;
     private VirtualMachine mVirtualMachine;
     private CursorHandler mCursorHandler;
     private ClipboardManager mClipboardManager;
-    private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
 
     private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
         VirtualMachineConfig.Builder configBuilder =
@@ -200,20 +205,32 @@
                 customImageConfigBuilder.setGpuConfig(gpuConfigBuilder.build());
             }
 
-            configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
+            long memoryMib = 1024; // 1GB by default
+            if (json.has("memory_mib")) {
+                memoryMib = json.getLong("memory_mib");
+            }
+            configBuilder.setMemoryBytes(memoryMib * 1024 * 1024);
+
             WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
-            Rect windowSize = windowMetrics.getBounds();
-            int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * windowMetrics.getDensity());
+            float dpi = DisplayMetrics.DENSITY_DEFAULT * windowMetrics.getDensity();
+            int refreshRate = (int) getDisplay().getRefreshRate();
+            if (json.has("display")) {
+                JSONObject display = json.getJSONObject("display");
+                if (display.has("scale")) {
+                    dpi *= (float) display.getDouble("scale");
+                }
+                if (display.has("refresh_rate")) {
+                    refreshRate = display.getInt("refresh_rate");
+                }
+            }
+            int dpiInt = (int) dpi;
             DisplayConfig.Builder displayConfigBuilder = new DisplayConfig.Builder();
+            Rect windowSize = windowMetrics.getBounds();
             displayConfigBuilder.setWidth(windowSize.right);
             displayConfigBuilder.setHeight(windowSize.bottom);
-            displayConfigBuilder.setHorizontalDpi(dpi);
-            displayConfigBuilder.setVerticalDpi(dpi);
-
-            Display display = getDisplay();
-            if (display != null) {
-                displayConfigBuilder.setRefreshRate((int) display.getRefreshRate());
-            }
+            displayConfigBuilder.setHorizontalDpi(dpiInt);
+            displayConfigBuilder.setVerticalDpi(dpiInt);
+            displayConfigBuilder.setRefreshRate(refreshRate);
 
             customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
             customImageConfigBuilder.useTouch(true);
@@ -310,6 +327,12 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        String action = getIntent().getAction();
+        if (!ACTION_VM_LAUNCHER.equals(action)) {
+            finish();
+            Log.e(TAG, "onCreate unsupported intent action: " + action);
+            return;
+        }
         checkAndRequestRecordAudioPermission();
         mExecutorService = Executors.newCachedThreadPool();
         try {
@@ -556,10 +579,11 @@
         Log.d(TAG, "destroyed");
     }
 
-    private static final int CLIPBOARD_SHARING_SERVER_PORT = 3580;
+    private static final int DATA_SHARING_SERVICE_PORT = 3580;
     private static final byte READ_CLIPBOARD_FROM_VM = 0;
     private static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
     private static final byte WRITE_CLIPBOARD_TYPE_TEXT_PLAIN = 2;
+    private static final byte OPEN_URL = 3;
 
     private ClipboardManager getClipboardManager() {
         if (mClipboardManager == null) {
@@ -581,16 +605,9 @@
         return header.array();
     }
 
-    private ParcelFileDescriptor connectClipboardSharingServer() {
-        ParcelFileDescriptor pfd;
-        try {
-            // TODO(349702313): Consider when clipboard sharing server is started to run in VM.
-            pfd = mVirtualMachine.connectVsock(CLIPBOARD_SHARING_SERVER_PORT);
-        } catch (VirtualMachineException e) {
-            Log.d(TAG, "cannot connect to the clipboard sharing server", e);
-            return null;
-        }
-        return pfd;
+    private ParcelFileDescriptor connectDataSharingService() throws VirtualMachineException {
+        // TODO(349702313): Consider when clipboard sharing server is started to run in VM.
+        return mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT);
     }
 
     private boolean writeClipboardToVm() {
@@ -604,64 +621,58 @@
         byte[] header =
                 constructClipboardHeader(
                         WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, text.getBytes().length + 1);
-        ParcelFileDescriptor pfd = connectClipboardSharingServer();
-        if (pfd == null) {
-            Log.d(TAG, "file descriptor of ClipboardSharingServer is null");
-            return false;
-        }
-        try (OutputStream stream = new AutoCloseOutputStream(pfd)) {
+        try (ParcelFileDescriptor pfd = connectDataSharingService();
+                OutputStream stream = new FileOutputStream(pfd.getFileDescriptor())) {
             stream.write(header);
             stream.write(text.getBytes());
             stream.write('\0');
-            stream.flush();
             Log.d(TAG, "successfully wrote clipboard data to the VM");
             return true;
-        } catch (IOException e) {
+        } catch (IOException | VirtualMachineException e) {
             Log.e(TAG, "failed to write clipboard data to the VM", e);
             return false;
         }
     }
 
+    private byte[] readExactly(InputStream stream, int len) throws IOException {
+        byte[] buf = stream.readNBytes(len);
+        if (buf.length != len) {
+            throw new IOException("Cannot read enough bytes");
+        }
+        return buf;
+    }
+
     private boolean readClipboardFromVm() {
         byte[] request = constructClipboardHeader(READ_CLIPBOARD_FROM_VM, 0);
-        ParcelFileDescriptor pfd = connectClipboardSharingServer();
-        if (pfd == null) {
-            Log.d(TAG, "file descriptor of ClipboardSharingServer is null");
-            return false;
-        }
-        try (OutputStream output = new AutoCloseOutputStream(pfd.dup())) {
-            output.write(request);
-            output.flush();
-            Log.d(TAG, "successfully send request to the VM for reading clipboard");
-        } catch (IOException e) {
-            Log.e(TAG, "failed to send request to the VM for read clipboard", e);
-            try {
-                pfd.close();
-            } catch (IOException err) {
-                Log.e(TAG, "failed to close file descriptor", err);
+        try (ParcelFileDescriptor pfd = connectDataSharingService()) {
+            try (OutputStream output = new FileOutputStream(pfd.getFileDescriptor())) {
+                output.write(request);
+                Log.d(TAG, "successfully send request to the VM for reading clipboard");
+            } catch (IOException e) {
+                Log.e(TAG, "failed to send request to the VM for read clipboard");
+                throw e;
             }
-            return false;
-        }
-
-        try (InputStream input = new AutoCloseInputStream(pfd)) {
-            ByteBuffer header = ByteBuffer.wrap(input.readNBytes(8));
-            header.order(ByteOrder.LITTLE_ENDIAN);
-            switch (header.get(0)) {
-                case WRITE_CLIPBOARD_TYPE_EMPTY:
-                    Log.d(TAG, "clipboard data in VM is empty");
-                    return true;
-                case WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
-                    int dataSize = header.getInt(4);
-                    String text_data =
-                            new String(input.readNBytes(dataSize), StandardCharsets.UTF_8);
-                    getClipboardManager().setPrimaryClip(ClipData.newPlainText(null, text_data));
-                    Log.d(TAG, "successfully received clipboard data from VM");
-                    return true;
-                default:
-                    Log.e(TAG, "unknown clipboard response type");
-                    return false;
+            try (InputStream input = new FileInputStream(pfd.getFileDescriptor())) {
+                ByteBuffer header = ByteBuffer.wrap(readExactly(input, 8));
+                header.order(ByteOrder.LITTLE_ENDIAN);
+                switch (header.get(0)) {
+                    case WRITE_CLIPBOARD_TYPE_EMPTY:
+                        Log.d(TAG, "clipboard data in VM is empty");
+                        return true;
+                    case WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
+                        int dataSize = header.getInt(4);
+                        String text_data =
+                                new String(readExactly(input, dataSize), StandardCharsets.UTF_8);
+                        getClipboardManager()
+                                .setPrimaryClip(ClipData.newPlainText(null, text_data));
+                        Log.d(TAG, "successfully received clipboard data from VM");
+                        return true;
+                    default:
+                        Log.e(TAG, "unknown clipboard response type");
+                        return false;
+                }
             }
-        } catch (IOException e) {
+        } catch (IOException | VirtualMachineException e) {
             Log.e(TAG, "failed to receive clipboard content from the VM", e);
             return false;
         }
@@ -676,16 +687,46 @@
             surfaceView.requestPointerCapture();
         }
         if (mVirtualMachine != null) {
-            if (hasFocus) {
-                Log.d(TAG, "writing clipboard of host device into VM");
-                writeClipboardToVm();
-            } else {
-                Log.d(TAG, "reading clipboard of VM");
-                readClipboardFromVm();
+            try {
+                if (hasFocus) {
+                    Log.d(TAG, "writing clipboard of host device into VM");
+                    writeClipboardToVm();
+                } else {
+                    Log.d(TAG, "reading clipboard of VM");
+                    readClipboardFromVm();
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "read/write clipboard error", e);
             }
         }
     }
 
+    @Override
+    protected void onNewIntent(Intent intent) {
+        String action = intent.getAction();
+        if (!ACTION_VM_OPEN_URL.equals(action)) {
+            Log.e(TAG, "onNewIntent unsupported intent action: " + action);
+            return;
+        }
+        Log.d(TAG, "onNewIntent intent action: " + action);
+        String text = intent.getStringExtra(Intent.EXTRA_TEXT);
+        if (text != null) {
+            mExecutorService.execute(
+                    () -> {
+                        byte[] data = text.getBytes();
+                        try (ParcelFileDescriptor pfd = connectDataSharingService();
+                                OutputStream stream =
+                                        new FileOutputStream(pfd.getFileDescriptor())) {
+                            stream.write(constructClipboardHeader(OPEN_URL, data.length));
+                            stream.write(data);
+                            Log.d(TAG, "Successfully sent URL to the VM");
+                        } catch (IOException | VirtualMachineException e) {
+                            Log.e(TAG, "Failed to send URL to the VM", e);
+                        }
+                    });
+        }
+    }
+
     @FunctionalInterface
     public interface RemoteExceptionCheckedFunction<T> {
         void apply(T t) throws RemoteException;