Define PortsStateManager managing shared pref for port forwarding

Bug: 380771744
Bug: 381319286
Test: Run VmTerminalApp

Change-Id: Ie8ba1ab60a85ca189a107a1f4ed792b7bfdd1491
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
index 93b0b0c..d167da3 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -19,7 +19,6 @@
 import static com.android.virtualization.terminal.MainActivity.TAG;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.util.Log;
 
 import androidx.annotation.Keep;
@@ -34,22 +33,13 @@
 
 import io.grpc.stub.StreamObserver;
 
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
 final class DebianServiceImpl extends DebianServiceGrpc.DebianServiceImplBase {
-    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 PortsStateManager mPortsStateManager;
+    private PortsStateManager.Listener mPortsStateListener;
     private final DebianServiceCallback mCallback;
 
     static {
@@ -60,12 +50,7 @@
         super();
         mCallback = callback;
         mContext = context;
-        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);
+        mPortsStateManager = PortsStateManager.getInstance(mContext);
     }
 
     @Override
@@ -73,23 +58,7 @@
             ReportVmActivePortsRequest request,
             StreamObserver<ReportVmActivePortsResponse> responseObserver) {
         Log.d(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(
-                    mPreferenceForwardingPortIsEnabled + Integer.toString(port))) {
-                editor.putBoolean(
-                        mPreferenceForwardingPortIsEnabled + Integer.toString(port), false);
-            }
-        }
-        editor.putStringSet(mPreferenceForwardingPorts, ports);
-        editor.apply();
-        mCallback.onActivePortsChanged(prevPorts, ports);
-
+        mPortsStateManager.updateActivePorts(new HashSet<>(request.getPortsList()));
         ReportVmActivePortsResponse reply =
                 ReportVmActivePortsResponse.newBuilder().setSuccess(true).build();
         responseObserver.onNext(reply);
@@ -110,18 +79,15 @@
     public void openForwardingRequestQueue(
             QueueOpeningRequest request, StreamObserver<ForwardingRequestItem> responseObserver) {
         Log.d(TAG, "OpenForwardingRequestQueue");
-        mPortForwardingListener =
-                new SharedPreferences.OnSharedPreferenceChangeListener() {
+        mPortsStateListener =
+                new PortsStateManager.Listener() {
                     @Override
-                    public void onSharedPreferenceChanged(
-                            SharedPreferences sharedPreferences, String key) {
-                        if (key.startsWith(mPreferenceForwardingPortIsEnabled)
-                                || key.equals(mPreferenceForwardingPorts)) {
-                            updateListeningPorts();
-                        }
+                    public void onPortsStateUpdated(
+                            Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
+                        updateListeningPorts();
                     }
                 };
-        mSharedPref.registerOnSharedPreferenceChangeListener(mPortForwardingListener);
+        mPortsStateManager.registerListener(mPortsStateListener);
         updateListeningPorts();
         runForwarderHost(request.getCid(), new ForwarderHostCallback(responseObserver));
         responseObserver.onCompleted();
@@ -151,31 +117,26 @@
 
     void killForwarderHost() {
         Log.d(TAG, "Stopping port forwarding");
-        if (mPortForwardingListener != null) {
-            mSharedPref.unregisterOnSharedPreferenceChangeListener(mPortForwardingListener);
-            terminateForwarderHost();
+        if (mPortsStateListener != null) {
+            mPortsStateManager.unregisterListener(mPortsStateListener);
+            mPortsStateListener = null;
         }
+        terminateForwarderHost();
     }
 
     private static native void updateListeningPorts(int[] ports);
 
     private void updateListeningPorts() {
+        Set<Integer> activePorts = mPortsStateManager.getActivePorts();
+        Set<Integer> enabledPorts = mPortsStateManager.getEnabledPorts();
         updateListeningPorts(
-                mSharedPref
-                        .getStringSet(mPreferenceForwardingPorts, Collections.emptySet())
-                        .stream()
-                        .filter(
-                                port ->
-                                        mSharedPref.getBoolean(
-                                                mPreferenceForwardingPortIsEnabled + port, false))
-                        .map(Integer::valueOf)
+                activePorts.stream()
+                        .filter(port -> enabledPorts.contains(port))
                         .mapToInt(Integer::intValue)
                         .toArray());
     }
 
     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/PortNotifier.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
index 2f728ba..0d70ab9 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortNotifier.java
@@ -27,7 +27,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.graphics.drawable.Icon;
-import android.util.Log;
 
 import java.util.HashSet;
 import java.util.Locale;
@@ -45,35 +44,39 @@
     private final Context mContext;
     private final NotificationManager mNotificationManager;
     private final BroadcastReceiver mReceiver;
+    private final PortsStateManager mPortsStateManager;
+    private final PortsStateManager.Listener mPortsStateListener;
 
     public PortNotifier(Context context) {
         mContext = context;
         mNotificationManager = mContext.getSystemService(NotificationManager.class);
         mReceiver = new PortForwardingRequestReceiver();
 
+        mPortsStateManager = PortsStateManager.getInstance(mContext);
+        mPortsStateListener =
+                new PortsStateManager.Listener() {
+                    @Override
+                    public void onPortsStateUpdated(
+                            Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
+                        Set<Integer> union = new HashSet<>(oldActivePorts);
+                        union.addAll(newActivePorts);
+                        for (int port : union) {
+                            if (!oldActivePorts.contains(port)) {
+                                showNotificationFor(port);
+                            } else if (!newActivePorts.contains(port)) {
+                                discardNotificationFor(port);
+                            }
+                        }
+                    }
+                };
+        mPortsStateManager.registerListener(mPortsStateListener);
+
         IntentFilter intentFilter = new IntentFilter(ACTION_PORT_FORWARDING);
         mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED);
     }
 
-    public void onActivePortsChanged(Set<String> oldPorts, Set<String> newPorts) {
-        Set<String> union = new HashSet<>(oldPorts);
-        union.addAll(newPorts);
-        for (String portStr : union) {
-            try {
-                int port = Integer.parseInt(portStr);
-                if (!oldPorts.contains(portStr)) {
-                    showNotificationFor(port);
-                } else if (!newPorts.contains(portStr)) {
-                    discardNotificationFor(port);
-                }
-            } catch (NumberFormatException e) {
-                Log.e(TAG, "Failed to parse port: " + portStr);
-                throw e;
-            }
-        }
-    }
-
     public void stop() {
+        mPortsStateManager.unregisterListener(mPortsStateListener);
         mContext.unregisterReceiver(mReceiver);
     }
 
@@ -134,16 +137,9 @@
         }
 
         private void performActionPortForwarding(Context context, Intent intent) {
-            String prefKey = context.getString(R.string.preference_file_key);
             int port = intent.getIntExtra(KEY_PORT, 0);
-            String key = context.getString(R.string.preference_forwarding_port_is_enabled) + port;
             boolean enabled = intent.getBooleanExtra(KEY_ENABLED, false);
-
-            context.getSharedPreferences(prefKey, Context.MODE_PRIVATE)
-                    .edit()
-                    .putBoolean(key, enabled)
-                    .apply();
-
+            mPortsStateManager.updateEnabledPort(port, enabled);
             discardNotificationFor(port);
         }
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
new file mode 100644
index 0000000..56ecd96
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/PortsStateManager.java
@@ -0,0 +1,146 @@
+/*
+ * 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.content.Context;
+import android.content.SharedPreferences;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * PortsStateManager is responsible for communicating with shared preferences and managing state of
+ * ports.
+ */
+public class PortsStateManager {
+    private static final String PREFS_NAME = ".PORTS";
+    private static final int FLAG_ENABLED = 1;
+
+    private static PortsStateManager mInstance;
+    private final Object mLock = new Object();
+
+    private final SharedPreferences mSharedPref;
+
+    @GuardedBy("mLock")
+    private Set<Integer> mActivePorts;
+
+    @GuardedBy("mLock")
+    private final Set<Integer> mEnabledPorts;
+
+    @GuardedBy("mLock")
+    private final Set<Listener> mListeners;
+
+    private PortsStateManager(SharedPreferences sharedPref) {
+        mSharedPref = sharedPref;
+        mEnabledPorts =
+                mSharedPref.getAll().entrySet().stream()
+                        .filter(entry -> entry.getValue() instanceof Integer)
+                        .filter(entry -> ((int) entry.getValue() & FLAG_ENABLED) == FLAG_ENABLED)
+                        .map(entry -> entry.getKey())
+                        .filter(
+                                key -> {
+                                    try {
+                                        Integer.parseInt(key);
+                                        return true;
+                                    } catch (NumberFormatException e) {
+                                        return false;
+                                    }
+                                })
+                        .map(Integer::parseInt)
+                        .collect(Collectors.toSet());
+        mActivePorts = new HashSet<>();
+        mListeners = new HashSet<>();
+    }
+
+    static synchronized PortsStateManager getInstance(Context context) {
+        if (mInstance == null) {
+            SharedPreferences sharedPref =
+                    context.getSharedPreferences(
+                            context.getPackageName() + PREFS_NAME, Context.MODE_PRIVATE);
+            mInstance = new PortsStateManager(sharedPref);
+        }
+        return mInstance;
+    }
+
+    Set<Integer> getActivePorts() {
+        synchronized (mLock) {
+            return new HashSet<>(mActivePorts);
+        }
+    }
+
+    Set<Integer> getEnabledPorts() {
+        synchronized (mLock) {
+            return new HashSet<>(mEnabledPorts);
+        }
+    }
+
+    void updateActivePorts(Set<Integer> ports) {
+        Set<Integer> oldPorts;
+        synchronized (mLock) {
+            oldPorts = mActivePorts;
+            mActivePorts = ports;
+        }
+        notifyPortsStateUpdated(oldPorts, ports);
+    }
+
+    void updateEnabledPort(int port, boolean enabled) {
+        Set<Integer> activePorts;
+        synchronized (mLock) {
+            SharedPreferences.Editor editor = mSharedPref.edit();
+            editor.putInt(String.valueOf(port), enabled ? FLAG_ENABLED : 0);
+            editor.apply();
+            if (enabled) {
+                mEnabledPorts.add(port);
+            } else {
+                mEnabledPorts.remove(port);
+            }
+            activePorts = mActivePorts;
+        }
+        notifyPortsStateUpdated(activePorts, activePorts);
+    }
+
+    void registerListener(Listener listener) {
+        synchronized (mLock) {
+            mListeners.add(listener);
+        }
+    }
+
+    void unregisterListener(Listener listener) {
+        synchronized (mLock) {
+            mListeners.remove(listener);
+        }
+    }
+
+    private void notifyPortsStateUpdated(Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {
+        Set<Listener> listeners;
+        synchronized (mLock) {
+            listeners = new HashSet<>(mListeners);
+        }
+        for (Listener listener : listeners) {
+            listener.onPortsStateUpdated(
+                    new HashSet<>(oldActivePorts), new HashSet<>(newActivePorts));
+        }
+    }
+
+    interface Listener {
+        default void onPortsStateUpdated(
+                Set<Integer> oldActivePorts, Set<Integer> newActivePorts) {}
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index fe693c4..d64c267 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -15,25 +15,21 @@
  */
 package com.android.virtualization.terminal
 
-import android.content.Context
-import android.content.SharedPreferences
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 
 class SettingsPortForwardingActivity : AppCompatActivity() {
-    private lateinit var mSharedPref: SharedPreferences
+    private lateinit var mPortsStateManager: PortsStateManager
     private lateinit var mAdapter: SettingsPortForwardingAdapter
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_port_forwarding)
 
-        mSharedPref =
-            this.getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE)
-
-        mAdapter = SettingsPortForwardingAdapter(mSharedPref, this)
+        mPortsStateManager = PortsStateManager.getInstance(this)
+        mAdapter = SettingsPortForwardingAdapter(mPortsStateManager)
 
         val recyclerView: RecyclerView = findViewById(R.id.settings_port_forwarding_recycler_view)
         recyclerView.layoutManager = LinearLayoutManager(this)
@@ -42,11 +38,11 @@
 
     override fun onResume() {
         super.onResume()
-        mSharedPref.registerOnSharedPreferenceChangeListener(mAdapter)
+        mAdapter.registerPortsStateListener()
     }
 
     override fun onPause() {
-        mSharedPref.unregisterOnSharedPreferenceChangeListener(mAdapter)
+        mAdapter.unregisterPortsStateListener()
         super.onPause()
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
index afe985a..8282910 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingAdapter.kt
@@ -15,8 +15,6 @@
  */
 package com.android.virtualization.terminal
 
-import android.content.Context
-import android.content.SharedPreferences
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -26,14 +24,11 @@
 import androidx.recyclerview.widget.SortedListAdapterCallback
 import com.google.android.material.materialswitch.MaterialSwitch
 
-class SettingsPortForwardingAdapter(
-    private val sharedPref: SharedPreferences?,
-    private val context: Context,
-) :
-    RecyclerView.Adapter<SettingsPortForwardingAdapter.ViewHolder>(),
-    SharedPreferences.OnSharedPreferenceChangeListener {
+class SettingsPortForwardingAdapter(private val mPortsStateManager: PortsStateManager) :
+    RecyclerView.Adapter<SettingsPortForwardingAdapter.ViewHolder>() {
 
     private var mItems: SortedList<SettingsPortForwardingItem>
+    private val mPortsStateListener: Listener
 
     init {
         mItems =
@@ -63,24 +58,24 @@
                 },
             )
         mItems.addAll(getCurrentSettingsPortForwardingItem())
+        mPortsStateListener = Listener()
+    }
+
+    fun registerPortsStateListener() {
+        mPortsStateManager.registerListener(mPortsStateListener)
+        mItems.replaceAll(getCurrentSettingsPortForwardingItem())
+    }
+
+    fun unregisterPortsStateListener() {
+        mPortsStateManager.unregisterListener(mPortsStateListener)
     }
 
     private fun getCurrentSettingsPortForwardingItem(): ArrayList<SettingsPortForwardingItem> {
-        val items = ArrayList<SettingsPortForwardingItem>()
-        val ports =
-            sharedPref!!.getStringSet(
-                context.getString(R.string.preference_forwarding_ports),
-                HashSet<String>(),
-            )
-        for (port in ports!!) {
-            val enabled =
-                sharedPref.getBoolean(
-                    context.getString(R.string.preference_forwarding_port_is_enabled) + port,
-                    false,
-                )
-            items.add(SettingsPortForwardingItem(port.toInt(), enabled))
-        }
-        return items
+        val enabledPorts = mPortsStateManager.getEnabledPorts()
+        return mPortsStateManager
+            .getActivePorts()
+            .map { SettingsPortForwardingItem(it, enabledPorts.contains(it)) }
+            .toCollection(ArrayList())
     }
 
     class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@@ -97,32 +92,19 @@
     }
 
     override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
-        viewHolder.port.text = mItems[position].port.toString()
+        val port = mItems[position].port
+        viewHolder.port.text = port.toString()
         viewHolder.enabledSwitch.contentDescription = viewHolder.port.text
         viewHolder.enabledSwitch.isChecked = mItems[position].enabled
         viewHolder.enabledSwitch.setOnCheckedChangeListener { _, isChecked ->
-            val sharedPref: SharedPreferences =
-                context.getSharedPreferences(
-                    context.getString(R.string.preference_file_key),
-                    Context.MODE_PRIVATE,
-                )
-            val editor = sharedPref.edit()
-            editor.putBoolean(
-                context.getString(R.string.preference_forwarding_port_is_enabled) +
-                    viewHolder.port.text,
-                isChecked,
-            )
-            editor.apply()
+            mPortsStateManager.updateEnabledPort(port, isChecked)
         }
     }
 
     override fun getItemCount() = mItems.size()
 
-    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
-        if (
-            key == context.getString(R.string.preference_forwarding_ports) ||
-                key!!.startsWith(context.getString(R.string.preference_forwarding_port_is_enabled))
-        ) {
+    private inner class Listener : PortsStateManager.Listener {
+        override fun onPortsStateUpdated(oldActivePorts: Set<Int>, newActivePorts: Set<Int>) {
             mItems.replaceAll(getCurrentSettingsPortForwardingItem())
         }
     }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index c2d224a..a82c688 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -291,11 +291,6 @@
         mResultReceiver.send(VmLauncherService.RESULT_IPADDR, b);
     }
 
-    @Override
-    public void onActivePortsChanged(Set<String> oldPorts, Set<String> newPorts) {
-        mPortNotifier.onActivePortsChanged(oldPorts, newPorts);
-    }
-
     public static void stop(Context context) {
         Intent i = getMyIntent(context);
         context.stopService(i);