Revamp exceptions

Various changes, based on the comments on aosp/2192077 and the API
guidelines:
- Always rethrow RemoteException as a runtime exception.
- Map the various other exceptions that VirtualizationService can
  return to VirtualMachineException for consistency - except for
  SecurityException (missing permissions) which will still be directly
  thrown.
- Document the exceptions that can be thrown and when for each API
  function.
- Always create the console & log pipes on demand, mostly so I don't
  have to document exactly when they might be null.
- Use an exception rather than an Optional for getCid().
- Check for null return from nativeConnectToVsockServer() and throw.

Bug: 243512115
Test: atest MicrodroidTests MicrodroidHostTests
Change-Id: Ib5cb421bc9137db4d9dddada71f0098212b7d0da
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachine.java b/javalib/src/android/system/virtualmachine/VirtualMachine.java
index 81f97f3..e2fc33e 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachine.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachine.java
@@ -53,6 +53,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
 import android.system.virtualizationcommon.ErrorCode;
 import android.system.virtualizationservice.DeathReason;
 import android.system.virtualizationservice.IVirtualMachine;
@@ -82,7 +83,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.WeakHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -286,6 +286,8 @@
         } catch (FileNotFoundException e) {
             throw new VirtualMachineException("instance image missing", e);
         } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        } catch (ServiceSpecificException | IllegalArgumentException e) {
             throw new VirtualMachineException("failed to create instance partition", e);
         }
 
@@ -379,7 +381,7 @@
      * @hide
      */
     @Status
-    public int getStatus() throws VirtualMachineException {
+    public int getStatus() {
         try {
             if (mVirtualMachine != null) {
                 switch (mVirtualMachine.getState()) {
@@ -395,7 +397,7 @@
                 }
             }
         } catch (RemoteException e) {
-            throw new VirtualMachineException(e);
+            throw e.rethrowAsRuntimeException();
         }
         if (!mConfigFilePath.exists()) {
             return STATUS_DELETED;
@@ -451,8 +453,11 @@
     /**
      * Runs this virtual machine. The returning of this method however doesn't mean that the VM has
      * actually started running or the OS has booted there. Such events can be notified by
-     * registering a callback using {@link #setCallback(Executor, VirtualMachineCallback)}.
+     * registering a callback using {@link #setCallback(Executor, VirtualMachineCallback)} before
+     * calling {@code run()}.
      *
+     * @throws VirtualMachineException if the virtual machine is not stopped or could not be
+     *         started.
      * @hide
      */
     @RequiresPermission(MANAGE_VIRTUAL_MACHINE_PERMISSION)
@@ -476,17 +481,7 @@
                         ServiceManager.waitForService(SERVICE_NAME));
 
         try {
-            if (mConsoleReader == null && mConsoleWriter == null) {
-                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
-                mConsoleReader = pipe[0];
-                mConsoleWriter = pipe[1];
-            }
-
-            if (mLogReader == null && mLogWriter == null) {
-                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
-                mLogReader = pipe[0];
-                mLogWriter = pipe[1];
-            }
+            createVmPipes();
 
             VirtualMachineAppConfig appConfig = getConfig().toParcel();
             appConfig.name = mName;
@@ -571,7 +566,27 @@
             );
             service.asBinder().linkToDeath(deathRecipient, 0);
             mVirtualMachine.start();
-        } catch (IOException | RemoteException e) {
+        } catch (IOException | IllegalStateException | ServiceSpecificException e) {
+            throw new VirtualMachineException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    private void createVmPipes() throws VirtualMachineException {
+        try {
+            if (mConsoleReader == null || mConsoleWriter == null) {
+                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                mConsoleReader = pipe[0];
+                mConsoleWriter = pipe[1];
+            }
+
+            if (mLogReader == null || mLogWriter == null) {
+                ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
+                mLogReader = pipe[0];
+                mLogWriter = pipe[1];
+            }
+        } catch (IOException e) {
             throw new VirtualMachineException(e);
         }
     }
@@ -633,26 +648,24 @@
     /**
      * Returns the stream object representing the console output from the virtual machine.
      *
+     * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
     @NonNull
     public InputStream getConsoleOutputStream() throws VirtualMachineException {
-        if (mConsoleReader == null) {
-            throw new VirtualMachineException("Console output not available");
-        }
+        createVmPipes();
         return new FileInputStream(mConsoleReader.getFileDescriptor());
     }
 
     /**
      * Returns the stream object representing the log output from the virtual machine.
      *
+     * @throws VirtualMachineException if the stream could not be created.
      * @hide
      */
     @NonNull
     public InputStream getLogOutputStream() throws VirtualMachineException {
-        if (mLogReader == null) {
-            throw new VirtualMachineException("Log output not available");
-        }
+        createVmPipes();
         return new FileInputStream(mLogReader.getFileDescriptor());
     }
 
@@ -662,6 +675,7 @@
      * notified of the event. A stopped virtual machine can be re-started by calling {@link
      * #run()}.
      *
+     * @throws VirtualMachineException if the virtual machine could not be stopped.
      * @hide
      */
     public void stop() throws VirtualMachineException {
@@ -670,6 +684,8 @@
             mVirtualMachine.stop();
             mVirtualMachine = null;
         } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        } catch (ServiceSpecificException e) {
             throw new VirtualMachineException(e);
         }
     }
@@ -677,6 +693,7 @@
     /**
      * Stops this virtual machine. See {@link #stop()}.
      *
+     * @throws VirtualMachineException if the virtual machine could not be stopped.
      * @hide
      */
     @Override
@@ -690,6 +707,7 @@
      * machine once deleted can never be restored. A new virtual machine created with the same name
      * and the same config is different from an already deleted virtual machine.
      *
+     * @throws VirtualMachineException if the virtual machine is not stopped.
      * @hide
      */
     public void delete() throws VirtualMachineException {
@@ -714,31 +732,32 @@
     /**
      * Returns the CID of this virtual machine, if it is running.
      *
+     * @throws VirtualMachineException if the virtual machine is not running.
      * @hide
      */
     @NonNull
-    public Optional<Integer> getCid() throws VirtualMachineException {
+    public int getCid() throws VirtualMachineException {
         if (getStatus() != STATUS_RUNNING) {
-            return Optional.empty();
+            throw new VirtualMachineException("VM is not running");
         }
         try {
-            return Optional.of(mVirtualMachine.getCid());
+            return mVirtualMachine.getCid();
         } catch (RemoteException e) {
-            throw new VirtualMachineException(e);
+            throw e.rethrowAsRuntimeException();
         }
     }
 
     /**
      * Changes the config of this virtual machine to a new one. This can be used to adjust things
      * like the number of CPU and size of the RAM, depending on the situation (e.g. the size of the
-     * application to run on the virtual machine, etc.) However, changing a config might make the
-     * virtual machine un-bootable if the new config is not compatible with the existing one. For
-     * example, if the signer of the app payload in the new config is different from that of the old
-     * config, the virtual machine won't boot. To prevent such cases, this method returns exception
-     * when an incompatible config is attempted.
+     * application to run on the virtual machine, etc.)
+     *
+     * The new config must be {@link VirtualMachineConfig#isCompatibleWith compatible with} the
+     * existing config.
      *
      * @return the old config
-     *
+     * @throws VirtualMachineException if the virtual machine is not stopped, or the new config is
+     *         incompatible.
      * @hide
      */
     @NonNull
@@ -765,6 +784,7 @@
         return oldConfig;
     }
 
+    @Nullable
     private static native IBinder nativeConnectToVsockServer(IBinder vmBinder, int port);
 
     /**
@@ -773,6 +793,8 @@
      * VirtualMachineCallback#onPayloadReady(VirtualMachine)}, it can use this method to
      * establish a connection to the guest VM.
      *
+     * @throws VirtualMachineException if the virtual machine is not running or the connection
+     *         failed.
      * @hide
      */
     @NonNull
@@ -780,12 +802,17 @@
         if (getStatus() != STATUS_RUNNING) {
             throw new VirtualMachineException("VM is not running");
         }
-        return nativeConnectToVsockServer(mVirtualMachine.asBinder(), port);
+        IBinder iBinder = nativeConnectToVsockServer(mVirtualMachine.asBinder(), port);
+        if (iBinder == null) {
+            throw new VirtualMachineException("Failed to connect to vsock server");
+        }
+        return iBinder;
     }
 
     /**
      * Opens a vsock connection to the VM on the given port.
      *
+     * @throws VirtualMachineException if connecting fails.
      * @hide
      */
     @NonNull
@@ -793,7 +820,9 @@
         try {
             return mVirtualMachine.connectVsock(port);
         } catch (RemoteException e) {
-            throw new VirtualMachineException("failed to connect vsock", e);
+            throw e.rethrowAsRuntimeException();
+        } catch (ServiceSpecificException e) {
+            throw new VirtualMachineException(e);
         }
     }
 
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index 9555d1e..2dff9bb 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -39,7 +39,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
-
 /**
  * Represents a configuration of a virtual machine. A configuration consists of hardware
  * configurations like the number of CPUs and the size of RAM, and software configurations like the
@@ -371,7 +370,7 @@
             }
 
             if (!mProtectedVmSet) {
-                throw new IllegalStateException("setProtectedVm(t/f) must be called explicitly");
+                throw new IllegalStateException("setProtectedVm must be called explicitly");
             }
 
             if (mProtectedVm
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
index 86fd91f..3f904be 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -120,7 +120,8 @@
      * name and the config are the same as a deleted one. The new virtual machine will initially
      * be stopped.
      *
-     * @throws VirtualMachineException If there is an existing virtual machine with the given name
+     * @throws VirtualMachineException if the VM cannot be created, or there is an existing VM with
+     *         the given name.
      * @hide
      */
     @NonNull
@@ -137,6 +138,7 @@
      * Returns an existing {@link VirtualMachine} with the given name. Returns null if there is no
      * such virtual machine.
      *
+     * @throws VirtualMachineException if the virtual machine could not be successfully retrieved.
      * @hide
      */
     @Nullable
@@ -148,6 +150,7 @@
      * Returns an existing {@link VirtualMachine} if it exists, or create a new one. The config
      * parameter is used only when a new virtual machine is created.
      *
+     * @throws VirtualMachineException if the virtual machine could not be created or retrieved.
      * @hide
      */
     @NonNull