Implement notification for port forwarding

Bug: 340126051
Bug: 376826982
Test: Run any server listening tcp port in the VM

Change-Id: I5fd4443330a497c7c56da6fd49b47d8c18a46587
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
index 0b65cf6..1b2ce8c 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -38,14 +38,11 @@
 
 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;
+    private final String mPreferenceForwardingPorts;
+    private final String mPreferenceForwardingPortIsEnabled;
     private SharedPreferences.OnSharedPreferenceChangeListener mPortForwardingListener;
     private final DebianServiceCallback mCallback;
 
@@ -57,7 +54,12 @@
         super();
         mCallback = callback;
         mContext = context;
-        mSharedPref = mContext.getSharedPreferences(PREFERENCE_FILE_KEY, Context.MODE_PRIVATE);
+        mSharedPref =
+                mContext.getSharedPreferences(
+                        mContext.getString(R.string.preference_file_key), Context.MODE_PRIVATE);
+        mPreferenceForwardingPorts = mContext.getString(R.string.preference_forwarding_ports);
+        mPreferenceForwardingPortIsEnabled =
+                mContext.getString(R.string.preference_forwarding_port_is_enabled);
     }
 
     @Override
@@ -66,19 +68,21 @@
             StreamObserver<ReportVmActivePortsResponse> responseObserver) {
         Log.d(DebianServiceImpl.TAG, "reportVmActivePorts: " + request.toString());
 
+        Set<String> prevPorts =
+                mSharedPref.getStringSet(mPreferenceForwardingPorts, Collections.emptySet());
         SharedPreferences.Editor editor = mSharedPref.edit();
         Set<String> ports = new HashSet<>();
         for (int port : request.getPortsList()) {
             ports.add(Integer.toString(port));
             if (!mSharedPref.contains(
-                    PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX + Integer.toString(port))) {
+                    mPreferenceForwardingPortIsEnabled + Integer.toString(port))) {
                 editor.putBoolean(
-                        PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX + Integer.toString(port),
-                        false);
+                        mPreferenceForwardingPortIsEnabled + Integer.toString(port), false);
             }
         }
-        editor.putStringSet(PREFERENCE_FORWARDING_PORTS, ports);
+        editor.putStringSet(mPreferenceForwardingPorts, ports);
         editor.apply();
+        mCallback.onActivePortsChanged(prevPorts, ports);
 
         ReportVmActivePortsResponse reply =
                 ReportVmActivePortsResponse.newBuilder().setSuccess(true).build();
@@ -105,8 +109,8 @@
                     @Override
                     public void onSharedPreferenceChanged(
                             SharedPreferences sharedPreferences, String key) {
-                        if (key.startsWith(PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX)
-                                || key.equals(PREFERENCE_FORWARDING_PORTS)) {
+                        if (key.startsWith(mPreferenceForwardingPortIsEnabled)
+                                || key.equals(mPreferenceForwardingPorts)) {
                             updateListeningPorts();
                         }
                     }
@@ -152,13 +156,12 @@
     private void updateListeningPorts() {
         updateListeningPorts(
                 mSharedPref
-                        .getStringSet(PREFERENCE_FORWARDING_PORTS, Collections.emptySet())
+                        .getStringSet(mPreferenceForwardingPorts, Collections.emptySet())
                         .stream()
                         .filter(
                                 port ->
                                         mSharedPref.getBoolean(
-                                                PREFERENCE_FORWARDING_PORT_IS_ENABLED_PREFIX + port,
-                                                false))
+                                                mPreferenceForwardingPortIsEnabled + port, false))
                         .map(Integer::valueOf)
                         .mapToInt(Integer::intValue)
                         .toArray());
@@ -166,5 +169,7 @@
 
     protected interface DebianServiceCallback {
         void onIpAddressAvailable(String ipAddr);
+
+        void onActivePortsChanged(Set<String> oldPorts, Set<String> newPorts);
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index a1509ad..32df273 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -62,48 +62,5 @@
         val recyclerView: RecyclerView = findViewById(R.id.settings_port_forwarding_recycler_view)
         recyclerView.layoutManager = LinearLayoutManager(this)
         recyclerView.adapter = settingsPortForwardingAdapter
-
-        // TODO: implement intent for accept, deny and tap to the notification
-        // Currently show a mock notification of a port opening
-        val terminalIntent = Intent()
-        val pendingIntent = PendingIntent.getActivity(
-            this, 0, terminalIntent,
-            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
-        )
-        val notification =
-            Notification.Builder(this, TAG)
-                .setChannelId(TAG)
-                .setSmallIcon(R.drawable.ic_launcher_foreground)
-                .setContentTitle(resources.getString(R.string.settings_port_forwarding_notification_title))
-                .setContentText(
-                    resources.getString(
-                        R.string.settings_port_forwarding_notification_content,
-                        8080
-                    )
-                )
-                .addAction(
-                    Notification.Action.Builder(
-                        Icon.createWithResource(resources, R.drawable.ic_launcher_foreground),
-                        resources.getString(R.string.settings_port_forwarding_notification_accept),
-                        pendingIntent
-                    ).build()
-                )
-                .addAction(
-                    Notification.Action.Builder(
-                        Icon.createWithResource(resources, R.drawable.ic_launcher_foreground),
-                        resources.getString(R.string.settings_port_forwarding_notification_deny),
-                        pendingIntent
-                    ).build()
-                )
-                .build()
-
-        with(NotificationManager.from(this)) {
-            if (ActivityCompat.checkSelfPermission(
-                    this@SettingsPortForwardingActivity, Manifest.permission.POST_NOTIFICATIONS
-                ) == PackageManager.PERMISSION_GRANTED
-            ) {
-                notify(0, notification)
-            }
-        }
     }
-}
\ 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 25afcb7..1c00c8d 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -17,8 +17,15 @@
 package com.android.virtualization.terminal;
 
 import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.ResultReceiver;
@@ -44,13 +51,15 @@
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.nio.file.Path;
+import java.util.HashSet;
 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";
-    private static final String TAG = "VmLauncherService";
+    static final String TAG = "VmLauncherService";
 
     private static final int RESULT_START = 0;
     private static final int RESULT_STOP = 1;
@@ -63,6 +72,7 @@
     private ResultReceiver mResultReceiver;
     private Server mServer;
     private DebianServiceImpl mDebianService;
+    private PortForwardingRequestReceiver mPortForwardingReceiver;
 
     @Override
     public IBinder onBind(Intent intent) {
@@ -130,6 +140,10 @@
 
         mResultReceiver.send(RESULT_START, null);
 
+        IntentFilter intentFilter =
+                new IntentFilter(PortForwardingRequestReceiver.ACTION_PORT_FORWARDING);
+        mPortForwardingReceiver = new PortForwardingRequestReceiver();
+        registerReceiver(mPortForwardingReceiver, intentFilter, RECEIVER_NOT_EXPORTED);
         startDebianServer();
 
         return START_NOT_STICKY;
@@ -137,7 +151,8 @@
 
     @Override
     public void onDestroy() {
-        super.onDestroy();
+        unregisterReceiver(mPortForwardingReceiver);
+        getSystemService(NotificationManager.class).cancelAll();
         stopDebianServer();
         if (mVirtualMachine != null) {
             if (mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING) {
@@ -152,6 +167,7 @@
             mExecutorService = null;
             mVirtualMachine = null;
         }
+        super.onDestroy();
     }
 
     private void startDebianServer() {
@@ -224,4 +240,102 @@
         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);
+        for (String portStr : union) {
+            try {
+                if (!oldPorts.contains(portStr)) {
+                    showPortForwardingNotification(Integer.parseInt(portStr));
+                } else if (!newPorts.contains(portStr)) {
+                    discardPortForwardingNotification(Integer.parseInt(portStr));
+                }
+            } catch (NumberFormatException e) {
+                Log.e(TAG, "Failed to parse port: " + portStr);
+                throw e;
+            }
+        }
+    }
+
+    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.putExtra(PortForwardingRequestReceiver.KEY_PORT, port);
+        intent.putExtra(PortForwardingRequestReceiver.KEY_ENABLED, enabled);
+        return PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+    }
+
+    private void showPortForwardingNotification(int port) {
+        Intent tapIntent = new Intent(this, SettingsPortForwardingActivity.class);
+        tapIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        PendingIntent tapPendingIntent =
+                PendingIntent.getActivity(this, 0, tapIntent, PendingIntent.FLAG_IMMUTABLE);
+
+        String title = getString(R.string.settings_port_forwarding_notification_title);
+        String content = getString(R.string.settings_port_forwarding_notification_content, port);
+        String acceptText = getString(R.string.settings_port_forwarding_notification_accept);
+        String denyText = getString(R.string.settings_port_forwarding_notification_deny);
+        Icon icon = Icon.createWithResource(this, R.drawable.ic_launcher_foreground);
+
+        Notification notification =
+                new Notification.Builder(this, this.getPackageName())
+                        .setSmallIcon(R.drawable.ic_launcher_foreground)
+                        .setContentTitle(title)
+                        .setContentText(content)
+                        .setContentIntent(tapPendingIntent)
+                        .addAction(
+                                new Notification.Action.Builder(
+                                                icon,
+                                                acceptText,
+                                                getPortForwardingPendingIntent(
+                                                        port, true /* enabled */))
+                                        .build())
+                        .addAction(
+                                new Notification.Action.Builder(
+                                                icon,
+                                                denyText,
+                                                getPortForwardingPendingIntent(
+                                                        port, false /* enabled */))
+                                        .build())
+                        .build();
+        getSystemService(NotificationManager.class).notify(TAG, port, notification);
+    }
+
+    private void discardPortForwardingNotification(int port) {
+        getSystemService(NotificationManager.class).cancel(TAG, port);
+    }
+
+    private final class PortForwardingRequestReceiver extends BroadcastReceiver {
+        private static final String ACTION_PORT_FORWARDING =
+                "android.virtualization.PORT_FORWARDING";
+        private static final String KEY_PORT = "port";
+        private static final String KEY_ENABLED = "enabled";
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (ACTION_PORT_FORWARDING.equals(intent.getAction())) {
+                performActionPortForwarding(context, intent);
+            }
+        }
+
+        private void performActionPortForwarding(Context context, Intent intent) {
+            int port = intent.getIntExtra(KEY_PORT, 0);
+            boolean enabled = intent.getBooleanExtra(KEY_ENABLED, false);
+
+            SharedPreferences sharedPref =
+                    context.getSharedPreferences(
+                            context.getString(R.string.preference_file_key), Context.MODE_PRIVATE);
+            SharedPreferences.Editor editor = sharedPref.edit();
+            editor.putBoolean(
+                    context.getString(R.string.preference_forwarding_port_is_enabled)
+                            + Integer.toString(port),
+                    enabled);
+            editor.apply();
+
+            context.getSystemService(NotificationManager.class).cancel(VmLauncherService.TAG, port);
+        }
+    }
 }
diff --git a/android/forwarder_host/src/forwarder_host.rs b/android/forwarder_host/src/forwarder_host.rs
index 2138957..ba427f5 100644
--- a/android/forwarder_host/src/forwarder_host.rs
+++ b/android/forwarder_host/src/forwarder_host.rs
@@ -384,6 +384,10 @@
     cid: jint,
     callback: JObject,
 ) {
+    // Clear shutdown event FD before running forwarder host.
+    SHUTDOWN_EVT.write(1).expect("Failed to write shutdown event FD");
+    SHUTDOWN_EVT.read().expect("Failed to consume shutdown event FD");
+
     match run_forwarder_host(cid, env, callback) {
         Ok(_) => {
             info!("forwarder_host is terminated");