Add equals, hashCode to MulticastRoutingConfig

Add helper functions to help compare if MulticastRoutingConfig
has changed.

Test: atest MulticastRoutingConfigTest
Change-Id: I30ed4796efbaafbcf1a273c12a9231ec02bc36f0
diff --git a/framework/src/android/net/MulticastRoutingConfig.java b/framework/src/android/net/MulticastRoutingConfig.java
index 1f4071c..4a3e1be 100644
--- a/framework/src/android/net/MulticastRoutingConfig.java
+++ b/framework/src/android/net/MulticastRoutingConfig.java
@@ -21,6 +21,7 @@
 import android.annotation.Nullable;
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 
@@ -30,7 +31,9 @@
 import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Objects;
 import java.util.Set;
+import java.util.StringJoiner;
 
 /**
  * A class representing a configuration for multicast routing.
@@ -162,15 +165,6 @@
         }
     };
 
-    @Override
-    public String toString() {
-        return "MulticastRoutingConfig{"
-                + "ForwardingMode=" + forwardingModeToString(mForwardingMode)
-                + ", MinScope=" + mMinScope
-                + ", ListeningAddresses=" + mListeningAddresses
-                + '}';
-    }
-
     private static String forwardingModeToString(final int forwardingMode) {
         switch (forwardingMode) {
             case FORWARD_NONE: return "NONE";
@@ -301,4 +295,41 @@
             default: return "unknown multicast routing mode " + mode;
         }
     }
+
+    public boolean equals(Object other) {
+        if (other == this) {
+            return true;
+        } else if (!(other instanceof MulticastRoutingConfig)) {
+            return false;
+        } else {
+            final MulticastRoutingConfig otherConfig = (MulticastRoutingConfig) other;
+            return mForwardingMode == otherConfig.mForwardingMode
+                && mMinScope == otherConfig.mMinScope
+                && mListeningAddresses.equals(otherConfig.mListeningAddresses);
+        }
+    }
+
+    public int hashCode() {
+        return Objects.hash(mForwardingMode, mMinScope, mListeningAddresses);
+    }
+
+    public String toString() {
+        final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}");
+
+        resultJoiner.add("ForwardingMode:");
+        resultJoiner.add(modeToString(mForwardingMode));
+
+        if (mForwardingMode == FORWARD_WITH_MIN_SCOPE) {
+            resultJoiner.add("MinScope:");
+            resultJoiner.add(Integer.toString(mMinScope));
+        }
+
+        if (mForwardingMode == FORWARD_SELECTED && !mListeningAddresses.isEmpty()) {
+            resultJoiner.add("ListeningAddresses: [");
+            resultJoiner.add(TextUtils.join(",", mListeningAddresses));
+            resultJoiner.add("]");
+        }
+
+        return resultJoiner.toString();
+    }
 }
diff --git a/tests/unit/java/android/net/MulticastRoutingConfigTest.kt b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
new file mode 100644
index 0000000..f01057b
--- /dev/null
+++ b/tests/unit/java/android/net/MulticastRoutingConfigTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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 android.net
+
+import android.os.Build
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.net.Inet6Address
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+import android.net.MulticastRoutingConfig.Builder
+import android.net.MulticastRoutingConfig.FORWARD_NONE
+import android.net.MulticastRoutingConfig.FORWARD_SELECTED
+import android.net.MulticastRoutingConfig.FORWARD_WITH_MIN_SCOPE
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+class MulticastRoutingConfigTest {
+
+    val address1 = Inet6Address.getByName("2000::8888") as Inet6Address
+    val address2 = Inet6Address.getByName("2000::9999") as Inet6Address
+
+    private fun configNone() = Builder(FORWARD_NONE).build()
+    private fun configMinScope(scope: Int) = Builder(FORWARD_WITH_MIN_SCOPE, scope).build()
+    private fun configSelected() = Builder(FORWARD_SELECTED).build()
+    private fun configSelectedWithAddress1AndAddress2() =
+            Builder(FORWARD_SELECTED).addListeningAddress(address1)
+            .addListeningAddress(address2).build()
+    private fun configSelectedWithAddress2AndAddress1() =
+            Builder(FORWARD_SELECTED).addListeningAddress(address2)
+            .addListeningAddress(address1).build()
+
+    @Test
+    fun equalityTests() {
+
+        assertTrue(configNone().equals(configNone()))
+
+        assertTrue(configSelected().equals(configSelected()))
+
+        assertTrue(configMinScope(4).equals(configMinScope(4)))
+
+        assertTrue(configSelectedWithAddress1AndAddress2()
+                .equals(configSelectedWithAddress2AndAddress1()))
+    }
+
+    @Test
+    fun inequalityTests() {
+
+        assertFalse(configNone().equals(configSelected()))
+
+        assertFalse(configNone().equals(configMinScope(4)))
+
+        assertFalse(configSelected().equals(configMinScope(4)))
+
+        assertFalse(configMinScope(4).equals(configMinScope(5)))
+
+        assertFalse(configSelected().equals(configSelectedWithAddress1AndAddress2()))
+    }
+
+    @Test
+    fun toString_equalObjects_returnsEqualStrings() {
+        val config1 = configSelectedWithAddress1AndAddress2()
+        val config2 = configSelectedWithAddress2AndAddress1()
+
+        val str1 = config1.toString()
+        val str2 = config2.toString()
+
+        assertTrue(str1.equals(str2))
+    }
+
+    @Test
+    fun toString_unequalObjects_returnsUnequalStrings() {
+        val config1 = configSelected()
+        val config2 = configSelectedWithAddress1AndAddress2()
+
+        val str1 = config1.toString()
+        val str2 = config2.toString()
+
+        assertFalse(str1.equals(str2))
+    }
+}