Merge "Enable mod keyboard in a11y env as well" into main
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index dad07ee..a9d6e9d 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -83,10 +83,6 @@
             <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>
 
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java b/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
index 01d2afa..fa5c382 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/CertificateUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.content.Context;
 import android.security.keystore.KeyGenParameterSpec;
 import android.security.keystore.KeyProperties;
@@ -37,8 +39,6 @@
 import java.security.cert.X509Certificate;
 
 public class CertificateUtils {
-    private static final String TAG = "CertificateUtils";
-
     private static final String ALIAS = "ttyd";
 
     public static KeyStore.PrivateKeyEntry createOrGetKey() {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
index 1b2ce8c..93b0b0c 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.util.Log;
@@ -37,7 +39,11 @@
 import java.util.Set;
 
 final class DebianServiceImpl extends DebianServiceGrpc.DebianServiceImplBase {
-    public static final String TAG = "DebianService";
+    private static final String PREFERENCE_FILE_KEY =
+            "com.android.virtualization.terminal.PREFERENCE_FILE_KEY";
+    private static final String PREFERENCE_FORWARDING_PORTS = "PREFERENCE_FORWARDING_PORTS";
+    private static final String PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX =
+            "PREFERENCE_FORWARDING_PORT_IS_ENABLED_";
 
     private final Context mContext;
     private final SharedPreferences mSharedPref;
@@ -66,7 +72,7 @@
     public void reportVmActivePorts(
             ReportVmActivePortsRequest request,
             StreamObserver<ReportVmActivePortsResponse> responseObserver) {
-        Log.d(DebianServiceImpl.TAG, "reportVmActivePorts: " + request.toString());
+        Log.d(TAG, "reportVmActivePorts: " + request.toString());
 
         Set<String> prevPorts =
                 mSharedPref.getStringSet(mPreferenceForwardingPorts, Collections.emptySet());
@@ -93,7 +99,7 @@
     @Override
     public void reportVmIpAddr(
             IpAddr request, StreamObserver<ReportVmIpAddrResponse> responseObserver) {
-        Log.d(DebianServiceImpl.TAG, "reportVmIpAddr: " + request.toString());
+        Log.d(TAG, "reportVmIpAddr: " + request.toString());
         mCallback.onIpAddressAvailable(request.getAddr());
         ReportVmIpAddrResponse reply = ReportVmIpAddrResponse.newBuilder().setSuccess(true).build();
         responseObserver.onNext(reply);
@@ -103,7 +109,7 @@
     @Override
     public void openForwardingRequestQueue(
             QueueOpeningRequest request, StreamObserver<ForwardingRequestItem> responseObserver) {
-        Log.d(DebianServiceImpl.TAG, "OpenForwardingRequestQueue");
+        Log.d(TAG, "OpenForwardingRequestQueue");
         mPortForwardingListener =
                 new SharedPreferences.OnSharedPreferenceChangeListener() {
                     @Override
@@ -144,7 +150,7 @@
     private static native void terminateForwarderHost();
 
     void killForwarderHost() {
-        Log.d(DebianServiceImpl.TAG, "Stopping port forwarding");
+        Log.d(TAG, "Stopping port forwarding");
         if (mPortForwardingListener != null) {
             mSharedPref.unregisterOnSharedPreferenceChangeListener(mPortForwardingListener);
             terminateForwarderHost();
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
index b17e636..71f2a2d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallUtils.java
@@ -15,6 +15,8 @@
  */
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.content.Context;
 import android.os.Environment;
 import android.os.FileUtils;
@@ -38,8 +40,6 @@
 import java.util.function.Function;
 
 public class InstallUtils {
-    private static final String TAG = InstallUtils.class.getSimpleName();
-
     private static final String VM_CONFIG_FILENAME = "vm_config.json";
     private static final String COMPRESSED_PAYLOAD_FILENAME = "images.tar.gz";
     private static final String ROOTFS_FILENAME = "root_part";
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 45da73c..52ef3d4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -16,6 +16,8 @@
 
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.annotation.MainThread;
 import android.content.ComponentName;
 import android.content.Context;
@@ -42,8 +44,6 @@
 import java.lang.ref.WeakReference;
 
 public class InstallerActivity extends BaseActivity {
-    private static final String TAG = "LinuxInstaller";
-
     private static final long ESTIMATED_IMG_SIZE_BYTES = FileUtils.parseSize("550MB");
 
     private CheckBox mWaitForWifiCheckbox;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
index 5d4c4ad..6fd3b5c 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerService.java
@@ -16,6 +16,8 @@
 
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.app.Notification;
 import android.app.PendingIntent;
 import android.app.Service;
@@ -55,8 +57,6 @@
 import java.util.concurrent.Executors;
 
 public class InstallerService extends Service {
-    private static final String TAG = "InstallerService";
-
     private static final int NOTIFICATION_ID = 1313; // any unique number among notifications
 
     private static final String IMAGE_URL =
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 15bc29c..bc5d037 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -76,7 +76,7 @@
 import java.util.Map;
 
 public class MainActivity extends BaseActivity
-        implements VmLauncherServices.VmLauncherServiceCallback, AccessibilityStateChangeListener {
+        implements VmLauncherService.VmLauncherServiceCallback, AccessibilityStateChangeListener {
     static final String TAG = "VmTerminalApp";
     private static final String VM_ADDR = "192.168.0.2";
     private static final int TTYD_PORT = 7681;
@@ -427,7 +427,7 @@
     @Override
     protected void onDestroy() {
         getSystemService(AccessibilityManager.class).removeAccessibilityStateChangeListener(this);
-        VmLauncherServices.stopVmLauncherService(this);
+        VmLauncherService.stop(this);
         super.onDestroy();
     }
 
@@ -539,7 +539,7 @@
 
         Intent stopIntent = new Intent();
         stopIntent.setClass(this, VmLauncherService.class);
-        stopIntent.setAction(VmLauncherServices.ACTION_STOP_VM_LAUNCHER_SERVICE);
+        stopIntent.setAction(VmLauncherService.ACTION_STOP_VM_LAUNCHER_SERVICE);
         PendingIntent stopPendingIntent =
                 PendingIntent.getService(
                         this,
@@ -577,7 +577,7 @@
                         .build();
 
         android.os.Trace.beginAsyncSection("executeTerminal", 0);
-        VmLauncherServices.startVmLauncherService(this, this, notification);
+        VmLauncherService.run(this, this, notification);
         connectToTerminalService();
     }
 
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java
index a2247b1..4094025 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Runner.java
@@ -16,6 +16,8 @@
 
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.content.Context;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
@@ -30,7 +32,6 @@
 
 /** Utility class for creating a VM and waiting for it to finish. */
 class Runner {
-    private static final String TAG = Runner.class.getSimpleName();
     private final VirtualMachine mVirtualMachine;
     private final Callback mCallback;
 
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index 32df273..1b39ff0 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -28,9 +28,9 @@
 import androidx.core.app.ActivityCompat
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
+import com.android.virtualization.terminal.MainActivity.TAG
 
 class SettingsPortForwardingActivity : AppCompatActivity() {
-    val TAG: String = "VmTerminalApp"
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_port_forwarding)
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
index ef76e03..e291b57 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsRecoveryActivity.kt
@@ -22,6 +22,7 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.view.isVisible
 import androidx.lifecycle.lifecycleScope
+import com.android.virtualization.terminal.MainActivity.TAG
 import com.google.android.material.card.MaterialCardView
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.snackbar.Snackbar
@@ -30,8 +31,6 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 
-const val TAG: String = "VmTerminalApp"
-
 class SettingsRecoveryActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -123,4 +122,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index 1c00c8d..cd1f65b 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -16,6 +16,8 @@
 
 package com.android.virtualization.terminal;
 
+import static com.android.virtualization.terminal.MainActivity.TAG;
+
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -27,7 +29,10 @@
 import android.content.SharedPreferences;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
 import android.os.ResultReceiver;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
@@ -52,14 +57,19 @@
 import java.net.InetSocketAddress;
 import java.nio.file.Path;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 public class VmLauncherService extends Service implements DebianServiceImpl.DebianServiceCallback {
-    public static final String EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION";
-    static final String TAG = "VmLauncherService";
+    private static final String EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION";
+    private static final String ACTION_START_VM_LAUNCHER_SERVICE =
+            "android.virtualization.START_VM_LAUNCHER_SERVICE";
+
+    public static final String ACTION_STOP_VM_LAUNCHER_SERVICE =
+            "android.virtualization.STOP_VM_LAUNCHER_SERVICE";
 
     private static final int RESULT_START = 0;
     private static final int RESULT_STOP = 1;
@@ -74,19 +84,71 @@
     private DebianServiceImpl mDebianService;
     private PortForwardingRequestReceiver mPortForwardingReceiver;
 
+    private static Intent getMyIntent(Context context) {
+        return new Intent(context.getApplicationContext(), VmLauncherService.class);
+    }
+
+    public interface VmLauncherServiceCallback {
+        void onVmStart();
+
+        void onVmStop();
+
+        void onVmError();
+
+        void onIpAddrAvailable(String ipAddr);
+    }
+
+    public static void run(
+            Context context, VmLauncherServiceCallback callback, Notification notification) {
+        Intent i = getMyIntent(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));
+        i.putExtra(VmLauncherService.EXTRA_NOTIFICATION, notification);
+        context.startForegroundService(i);
+    }
+
+    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;
+    }
+
     @Override
     public IBinder onBind(Intent intent) {
         return null;
     }
 
-    private void startForeground(Notification notification) {
-        startForeground(this.hashCode(), notification);
-    }
-
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
-        if (Objects.equals(
-                intent.getAction(), VmLauncherServices.ACTION_STOP_VM_LAUNCHER_SERVICE)) {
+        if (Objects.equals(intent.getAction(), ACTION_STOP_VM_LAUNCHER_SERVICE)) {
             stopSelf();
             return START_NOT_STICKY;
         }
@@ -136,7 +198,7 @@
         Notification notification =
                 intent.getParcelableExtra(EXTRA_NOTIFICATION, Notification.class);
 
-        startForeground(notification);
+        startForeground(this.hashCode(), notification);
 
         mResultReceiver.send(RESULT_START, null);
 
@@ -149,27 +211,6 @@
         return START_NOT_STICKY;
     }
 
-    @Override
-    public void onDestroy() {
-        unregisterReceiver(mPortForwardingReceiver);
-        getSystemService(NotificationManager.class).cancelAll();
-        stopDebianServer();
-        if (mVirtualMachine != null) {
-            if (mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING) {
-                try {
-                    mVirtualMachine.stop();
-                    stopForeground(STOP_FOREGROUND_REMOVE);
-                } catch (VirtualMachineException e) {
-                    Log.e(TAG, "failed to stop a VM instance", e);
-                }
-            }
-            mExecutorService.shutdownNow();
-            mExecutorService = null;
-            mVirtualMachine = null;
-        }
-        super.onDestroy();
-    }
-
     private void startDebianServer() {
         ServerInterceptor interceptor =
                 new ServerInterceptor() {
@@ -224,6 +265,40 @@
                 });
     }
 
+    @Override
+    public void onIpAddressAvailable(String ipAddr) {
+        android.os.Trace.endAsyncSection("debianBoot", 0);
+        Bundle b = new Bundle();
+        b.putString(VmLauncherService.KEY_VM_IP_ADDR, ipAddr);
+        mResultReceiver.send(VmLauncherService.RESULT_IPADDR, b);
+    }
+
+    public static void stop(Context context) {
+        Intent i = getMyIntent(context);
+        context.stopService(i);
+    }
+
+    @Override
+    public void onDestroy() {
+        unregisterReceiver(mPortForwardingReceiver);
+        getSystemService(NotificationManager.class).cancelAll();
+        stopDebianServer();
+        if (mVirtualMachine != null) {
+            if (mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING) {
+                try {
+                    mVirtualMachine.stop();
+                    stopForeground(STOP_FOREGROUND_REMOVE);
+                } catch (VirtualMachineException e) {
+                    Log.e(TAG, "failed to stop a VM instance", e);
+                }
+            }
+            mExecutorService.shutdownNow();
+            mExecutorService = null;
+            mVirtualMachine = null;
+        }
+        super.onDestroy();
+    }
+
     private void stopDebianServer() {
         if (mDebianService != null) {
             mDebianService.killForwarderHost();
@@ -234,14 +309,6 @@
     }
 
     @Override
-    public void onIpAddressAvailable(String ipAddr) {
-        android.os.Trace.endAsyncSection("debianBoot", 0);
-        Bundle b = new Bundle();
-        b.putString(VmLauncherService.KEY_VM_IP_ADDR, ipAddr);
-        mResultReceiver.send(VmLauncherService.RESULT_IPADDR, b);
-    }
-
-    @Override
     public void onActivePortsChanged(Set<String> oldPorts, Set<String> newPorts) {
         Set<String> union = new HashSet<>(oldPorts);
         union.addAll(newPorts);
@@ -262,7 +329,7 @@
     private PendingIntent getPortForwardingPendingIntent(int port, boolean enabled) {
         Intent intent = new Intent(PortForwardingRequestReceiver.ACTION_PORT_FORWARDING);
         intent.setPackage(getPackageName());
-        intent.setIdentifier(String.format("%d_%b", port, enabled));
+        intent.setIdentifier(String.format(Locale.ROOT, "%d_%b", port, enabled));
         intent.putExtra(PortForwardingRequestReceiver.KEY_PORT, port);
         intent.putExtra(PortForwardingRequestReceiver.KEY_ENABLED, enabled);
         return PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
@@ -308,7 +375,7 @@
         getSystemService(NotificationManager.class).cancel(TAG, port);
     }
 
-    private final class PortForwardingRequestReceiver extends BroadcastReceiver {
+    private static final class PortForwardingRequestReceiver extends BroadcastReceiver {
         private static final String ACTION_PORT_FORWARDING =
                 "android.virtualization.PORT_FORWARDING";
         private static final String KEY_PORT = "port";
@@ -335,7 +402,7 @@
                     enabled);
             editor.apply();
 
-            context.getSystemService(NotificationManager.class).cancel(VmLauncherService.TAG, port);
+            context.getSystemService(NotificationManager.class).cancel(TAG, port);
         }
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherServices.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherServices.java
deleted file mode 100644
index d6c6786..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherServices.java
+++ /dev/null
@@ -1,122 +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.app.Notification;
-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";
-
-    public static final String ACTION_STOP_VM_LAUNCHER_SERVICE =
-            "android.virtualization.STOP_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 stopVmLauncherService(Context context) {
-        Intent i = buildVmLauncherServiceIntent(context);
-        context.stopService(i);
-    }
-
-    public static void startVmLauncherService(
-            Context context, VmLauncherServiceCallback callback, Notification notification) {
-        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));
-        i.putExtra(VmLauncherService.EXTRA_NOTIFICATION, notification);
-        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;
-    }
-}
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 9a9ede4..eaff2a4 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -24,3 +24,22 @@
 
 The `vm` command also has other subcommands for debugging; run
 `/apex/com.android.virt/bin/vm help` for details.
+
+# Terminal app
+## Graphical environment (Wayland, VNC)
+By installing Wayland compositor and VNC backend, you can enable graphical environment.
+One of the options is `sway`, `wayvnc` and `xwayland`(if necessary).
+
+```
+sudo apt install sway wayvnc xwayland
+WLR_BACKENDS=headless WLR_LIBINPUT_NO_DEVICES=1 sway
+WAYLAND_DISPLAY=wayland-1 wayvnc 0.0.0.0 # or use port forwarding
+```
+
+And then, connect to 192.168.0.2:5900(or localhost:5900) with arbitrary VNC client.
+Or, `novnc`(https://github.com/novnc/noVNC/releases). For `novnc` you need to install
+`novnc`, and run `<novnc_path>/utils/novnc_proxy`, and then connect to `http://192.168.0.2:6080/vnc.html`
+(or `localhost:6080` if port forwarding is enabled.)
+
+`weston` with VNC backend might be another option, but it isn't available in
+Debian package repository for bookworm.
\ No newline at end of file
diff --git a/guest/forwarder_guest_launcher/src/main.rs b/guest/forwarder_guest_launcher/src/main.rs
index 0bb3b4d..1da37b4 100644
--- a/guest/forwarder_guest_launcher/src/main.rs
+++ b/guest/forwarder_guest_launcher/src/main.rs
@@ -36,6 +36,7 @@
 
 const NON_PREVILEGED_PORT_RANGE_START: i32 = 1024;
 const TCPSTATES_IP_4: i8 = 4;
+const TCPSTATES_STATE_CLOSE: &str = "CLOSE";
 const TCPSTATES_STATE_LISTEN: &str = "LISTEN";
 
 #[derive(Debug, Deserialize)]
@@ -43,7 +44,7 @@
 struct TcpStateRow {
     ip: i8,
     lport: i32,
-    oldstate: String,
+    rport: i32,
     newstate: String,
 }
 
@@ -142,14 +143,17 @@
         if row.lport < NON_PREVILEGED_PORT_RANGE_START {
             continue;
         }
-        match (row.oldstate.as_str(), row.newstate.as_str()) {
-            (_, TCPSTATES_STATE_LISTEN) => {
+        if row.rport > 0 {
+            continue;
+        }
+        match row.newstate.as_str() {
+            TCPSTATES_STATE_LISTEN => {
                 listening_ports.insert(row.lport);
             }
-            (TCPSTATES_STATE_LISTEN, _) => {
+            TCPSTATES_STATE_CLOSE => {
                 listening_ports.remove(&row.lport);
             }
-            (_, _) => continue,
+            _ => continue,
         }
         send_active_ports_report(listening_ports.clone(), &mut client).await?;
     }