Define VmLauncherService
It will be useful to create 'headless' vm. In addition, it has
vm_launcher_lib for easy service call outside.
Bug: 357827587
Test: build
Change-Id: I371974566d9d86bff1ee1dfc83bf81ce25ace8d5
diff --git a/android/VmLauncherApp/AndroidManifest.xml b/android/VmLauncherApp/AndroidManifest.xml
index 67b7a45..583fce7 100644
--- a/android/VmLauncherApp/AndroidManifest.xml
+++ b/android/VmLauncherApp/AndroidManifest.xml
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-feature android:name="android.software.virtualization_framework" android:required="true" />
<permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
@@ -26,6 +28,21 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <service
+ android:name=".VmLauncherService"
+ android:enabled="true"
+ android:exported="true"
+ android:permission="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
+ android:foregroundServiceType="specialUse">
+ <property
+ android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+ android:value="Run VM instances" />
+ <intent-filter>
+ <action android:name="android.virtualization.START_VM_LAUNCHER_SERVICE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </service>
+
</application>
</manifest>
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
new file mode 100644
index 0000000..ec98f4c
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -0,0 +1,149 @@
+/*
+ * 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.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.ResultReceiver;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class VmLauncherService extends Service {
+ private static final String TAG = "VmLauncherService";
+ // TODO: this path should be from outside of this service
+ private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
+
+ private static final int RESULT_START = 0;
+ private static final int RESULT_STOP = 1;
+ private static final int RESULT_ERROR = 2;
+ private static final int RESULT_IPADDR = 3;
+ private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+ private ExecutorService mExecutorService;
+ private VirtualMachine mVirtualMachine;
+ private ResultReceiver mResultReceiver;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private void startForeground() {
+ NotificationManager notificationManager = getSystemService(NotificationManager.class);
+ NotificationChannel notificationChannel =
+ new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+ notificationManager.createNotificationChannel(notificationChannel);
+ startForeground(
+ this.hashCode(),
+ new Notification.Builder(this, TAG)
+ .setChannelId(TAG)
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setContentText("A VM " + mVirtualMachine.getName() + " is running")
+ .build());
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ mExecutorService = Executors.newCachedThreadPool();
+
+ ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+ VirtualMachineConfig config = json.toConfig(this);
+
+ Runner runner;
+ try {
+ runner = Runner.create(this, config);
+ } catch (VirtualMachineException e) {
+ throw new RuntimeException(e);
+ }
+ mVirtualMachine = runner.getVm();
+ mResultReceiver =
+ intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver.class);
+
+ runner.getExitStatus()
+ .thenAcceptAsync(
+ success -> {
+ if (mResultReceiver != null) {
+ mResultReceiver.send(success ? RESULT_STOP : RESULT_ERROR, null);
+ }
+ if (!success) {
+ stopSelf();
+ }
+ });
+ Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
+ Logger.setup(mVirtualMachine, logPath, mExecutorService);
+
+ startForeground();
+
+ mResultReceiver.send(RESULT_START, null);
+ if (config.getCustomImageConfig().useNetwork()) {
+ Handler handler = new Handler(Looper.getMainLooper());
+ gatherIpAddrFromVm(handler);
+ }
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mExecutorService.shutdownNow();
+ }
+
+ // TODO(b/359523803): Use AVF API to get ip addr when it exists
+ private void gatherIpAddrFromVm(Handler handler) {
+ handler.postDelayed(
+ () -> {
+ int INTERNAL_VSOCK_SERVER_PORT = 1024;
+ try (ParcelFileDescriptor pfd =
+ mVirtualMachine.connectVsock(INTERNAL_VSOCK_SERVER_PORT)) {
+ try (BufferedReader input =
+ new BufferedReader(
+ new InputStreamReader(
+ new FileInputStream(pfd.getFileDescriptor())))) {
+ String vmIpAddr = input.readLine().strip();
+ Bundle b = new Bundle();
+ b.putString(KEY_VM_IP_ADDR, vmIpAddr);
+ mResultReceiver.send(RESULT_IPADDR, b);
+ return;
+ } catch (IOException e) {
+ Log.e(TAG, e.toString());
+ }
+ } catch (Exception e) {
+ Log.e(TAG, e.toString());
+ }
+ gatherIpAddrFromVm(handler);
+ },
+ 1000);
+ }
+}
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
new file mode 100644
index 0000000..8591c8d
--- /dev/null
+++ b/libs/vm_launcher_lib/Android.bp
@@ -0,0 +1,13 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "vm_launcher_lib",
+ srcs: ["java/**/*.java"],
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.virt",
+ ],
+ sdk_version: "system_current",
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
new file mode 100644
index 0000000..c5bc5fb
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
@@ -0,0 +1,112 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import java.util.List;
+
+public class VmLauncherServices {
+ private static final String TAG = "VmLauncherServices";
+
+ private static final String ACTION_START_VM_LAUNCHER_SERVICE =
+ "android.virtualization.START_VM_LAUNCHER_SERVICE";
+
+ private static final int RESULT_START = 0;
+ private static final int RESULT_STOP = 1;
+ private static final int RESULT_ERROR = 2;
+ private static final int RESULT_IPADDR = 3;
+ private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+ private static Intent buildVmLauncherServiceIntent(Context context) {
+ Intent i = new Intent();
+ i.setAction(ACTION_START_VM_LAUNCHER_SERVICE);
+
+ Intent intent = new Intent(ACTION_START_VM_LAUNCHER_SERVICE);
+ PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> resolveInfos =
+ pm.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfos == null || resolveInfos.size() != 1) {
+ Log.e(TAG, "cannot find a service to handle ACTION_START_VM_LAUNCHER_SERVICE");
+ return null;
+ }
+ String packageName = resolveInfos.get(0).serviceInfo.packageName;
+
+ i.setPackage(packageName);
+ return i;
+ }
+
+ public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback) {
+ Intent i = buildVmLauncherServiceIntent(context);
+ if (i == null) {
+ return;
+ }
+ ResultReceiver resultReceiver =
+ new ResultReceiver(new Handler(Looper.myLooper())) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (callback == null) {
+ return;
+ }
+ switch (resultCode) {
+ case RESULT_START:
+ callback.onVmStart();
+ return;
+ case RESULT_STOP:
+ callback.onVmStop();
+ return;
+ case RESULT_ERROR:
+ callback.onVmError();
+ return;
+ case RESULT_IPADDR:
+ callback.onIpAddrAvailable(resultData.getString(KEY_VM_IP_ADDR));
+ return;
+ }
+ }
+ };
+ i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver));
+ context.startForegroundService(i);
+ }
+
+ public interface VmLauncherServiceCallback {
+ void onVmStart();
+
+ void onVmStop();
+
+ void onVmError();
+
+ void onIpAddrAvailable(String ipAddr);
+ }
+
+ private static ResultReceiver getResultReceiverForIntent(ResultReceiver r) {
+ Parcel parcel = Parcel.obtain();
+ r.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ r = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return r;
+ }
+}