VmLauncherApp: Handle OPEN_URL actions in a handler thread

* When FerrochromeApp is started, enqueue any OPEN_URL Intent to the
  thread.
* In the thread, wait and retry until VmAgent connects (indicates VM
  boot ready).
* As soon as VmAgent connects, send the OPEN_URL request to VM.
* After the user is switched to the VM activity and unlock the VM
  screen, the URL should be open in the VM browser.

Note: Only works consistently if using a non-guest account. If using a
guest account, or switching account, the VM side OPEN_URL request
handling might fail. This still needs investigating.

Bug: 348303697
Test: 0. Ferrochrome OS must already have an user account set up
  1. Close the FerrochromeApp
  2. Open browser and share a hyperlink to FerrochromeApp
  3. FerrochromeApp should launch
  4. Unlock Ferrochrome (enter the password / pin of the existing
     account)
  5. The shared link should be opened by the Ferrochrome browser
Change-Id: I99f9afe7c1485cdfe2703a3e4618fb3bb308a122
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
index c32d017..433e89c 100644
--- a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
+++ b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
@@ -30,8 +30,8 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        boolean isRoot = isTaskRoot();
         finish();
+
         if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
             return;
         }
@@ -49,16 +49,6 @@
             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(
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
index 828d923..def464e 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
@@ -34,7 +34,7 @@
         mVmAgent = vmAgent;
     }
 
-    private VmAgent.Connection getConnection() {
+    private VmAgent.Connection getConnection() throws InterruptedException {
         return mVmAgent.connect();
     }
 
@@ -53,7 +53,7 @@
 
         try {
             getConnection().sendData(VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, data);
-        } catch (RuntimeException e) {
+        } catch (InterruptedException | RuntimeException e) {
             Log.e(TAG, "Failed to write clipboard data to VM", e);
         }
     }
@@ -63,7 +63,7 @@
         VmAgent.Data data;
         try {
             data = getConnection().sendAndReceive(VmAgent.READ_CLIPBOARD_FROM_VM, null);
-        } catch (RuntimeException e) {
+        } catch (InterruptedException | RuntimeException e) {
             Log.e(TAG, "Failed to read clipboard data from VM", e);
             return;
         }
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index 54543b0..fb75533 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -52,16 +52,12 @@
     private DisplayProvider mDisplayProvider;
     private VmAgent mVmAgent;
     private ClipboardHandler mClipboardHandler;
+    private OpenUrlHandler mOpenUrlHandler;
 
     @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;
-        }
+        Log.d(TAG, "onCreate intent: " + getIntent());
         checkAndRequestRecordAudioPermission();
         mExecutorService = Executors.newCachedThreadPool();
 
@@ -98,6 +94,8 @@
 
         mVmAgent = new VmAgent(mVirtualMachine);
         mClipboardHandler = new ClipboardHandler(this, mVmAgent);
+        mOpenUrlHandler = new OpenUrlHandler(mVmAgent);
+        handleIntent(getIntent());
     }
 
     private void makeFullscreen() {
@@ -146,6 +144,7 @@
         super.onDestroy();
         mExecutorService.shutdownNow();
         mInputForwarder.cleanUp();
+        mOpenUrlHandler.shutdown();
         Log.d(TAG, "destroyed");
     }
 
@@ -172,18 +171,16 @@
 
     @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(
-                    () -> {
-                        mVmAgent.connect().sendData(VmAgent.OPEN_URL, text.getBytes());
-                    });
+        Log.d(TAG, "onNewIntent intent: " + intent);
+        handleIntent(intent);
+    }
+
+    private void handleIntent(Intent intent) {
+        if (ACTION_VM_OPEN_URL.equals(intent.getAction())) {
+            String url = intent.getStringExtra(Intent.EXTRA_TEXT);
+            if (url != null) {
+                mOpenUrlHandler.sendUrlToVm(url);
+            }
         }
     }
 
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
new file mode 100644
index 0000000..fb0c6bf
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
@@ -0,0 +1,50 @@
+/*
+ * 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.vmlauncher;
+
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+class OpenUrlHandler {
+    private static final String TAG = MainActivity.TAG;
+
+    private final VmAgent mVmAgent;
+    private final ExecutorService mExecutorService;
+
+    OpenUrlHandler(VmAgent vmAgent) {
+        mVmAgent = vmAgent;
+        mExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    void shutdown() {
+        mExecutorService.shutdownNow();
+    }
+
+    void sendUrlToVm(String url) {
+        mExecutorService.execute(
+                () -> {
+                    try {
+                        mVmAgent.connect().sendData(VmAgent.OPEN_URL, url.getBytes());
+                        Log.d(TAG, "Successfully sent URL to the VM");
+                    } catch (InterruptedException | RuntimeException e) {
+                        Log.e(TAG, "Failed to send URL to the VM", e);
+                    }
+                });
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
index 78da6c0..af1d298 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
@@ -17,8 +17,10 @@
 package com.android.virtualization.vmlauncher;
 
 import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
 
 import libcore.io.Streams;
 
@@ -39,6 +41,7 @@
     private static final int DATA_SHARING_SERVICE_PORT = 3580;
     private static final int HEADER_SIZE = 8; // size of the header
     private static final int SIZE_OFFSET = 4; // offset of the size field in the header
+    private static final long RETRY_INTERVAL_MS = 1_000;
 
     static final byte READ_CLIPBOARD_FROM_VM = 0;
     static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
@@ -51,13 +54,26 @@
         mVirtualMachine = vm;
     }
 
-    /** Connect to the agent and returns the communication channel established. This can block. */
-    Connection connect() {
-        try {
-            // TODO: wait until the VM is up and the agent is running
-            return new Connection(mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT));
-        } catch (VirtualMachineException e) {
-            throw new RuntimeException("Failed to connect to the VM agent", e);
+    /**
+     * Connects to the agent and returns the established communication channel. This can block.
+     *
+     * @throws InterruptedException If the current thread was interrupted
+     */
+    Connection connect() throws InterruptedException {
+        boolean shouldLog = true;
+        while (true) {
+            if (Thread.interrupted()) {
+                throw new InterruptedException();
+            }
+            try {
+                return new Connection(mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT));
+            } catch (VirtualMachineException e) {
+                if (shouldLog) {
+                    shouldLog = false;
+                    Log.d(TAG, "Still waiting for VM agent to start", e);
+                }
+            }
+            SystemClock.sleep(RETRY_INTERVAL_MS);
         }
     }