[mDNS] pretty print txt values in log

Without this commit, the Thread service logs look like:

```
03-27 14:50:07.033   739  1924 I NsdPublisher: Registering service. Listener ID: 1, serviceInfo: name: Android Thread Border Router #2CF8, type: _meshcop._udp, subtypes: , hostAddresses: , hostname: null, port: 49152, network: null, expirationTime: null, txtRecord: id=֞�"ҽD���.�j���nn=OpenThreadrv=1sb=������tv=1.3.0
03-27 14:50:07.033   739  1924 I NsdPublisher: vn=Android
                                                         xa=Q�;�P,�
                                                                   xp=ޭ��������

```

With this commit, the Thread service logs look like:

```
03-27 13:37:31.672   741  1925 I NsdPublisher: Registering service. Listener ID: 1, serviceInfo: name: Android Thread Border Router #7CE8, type: _meshcop._udp, subtypes: , hostAddresses: , hostname: null, port: 49152, network: null, expirationTime: null, txtRecord: {id=0x70A5AE087668D7522919F40D1684D182, mn=Thread Border Router, nn=OpenThread, rv=1, sb=0x00000021, tv=1.3.0, vn=Android, xa=0x06EC93E283A47CE8, xp=0xDEAD00BEEF00CAFE}
```

Bug: 329366417
Change-Id: I6b15e93ad117f4ebb5762df85e359a705bd65b7d
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index dba08a1..9491a9c 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,8 @@
 
 package android.net.nsd;
 
+import static com.android.net.module.util.HexDump.toHexString;
+
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -39,6 +41,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.StringJoiner;
 
 /**
  * A class representing service information for network service discovery
@@ -541,11 +544,50 @@
                 .append(", network: ").append(mNetwork)
                 .append(", expirationTime: ").append(mExpirationTime);
 
-        byte[] txtRecord = getTxtRecord();
-        sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
+        final StringJoiner txtJoiner =
+                new StringJoiner(", " /* delimiter */, "{" /* prefix */, "}" /* suffix */);
+
+        sb.append(", txtRecord: ");
+        for (int i = 0; i < mTxtRecord.size(); i++) {
+            txtJoiner.add(mTxtRecord.keyAt(i) + "=" + getPrintableTxtValue(mTxtRecord.valueAt(i)));
+        }
+        sb.append(txtJoiner.toString());
         return sb.toString();
     }
 
+    /**
+     * Returns printable string for {@code txtValue}.
+     *
+     * If {@code txtValue} contains non-printable ASCII characters, a HEX string with prefix "0x"
+     * will be returned. Otherwise, the ASCII string of {@code txtValue} is returned.
+     *
+     */
+    private static String getPrintableTxtValue(@Nullable byte[] txtValue) {
+        if (txtValue == null) {
+            return "(null)";
+        }
+
+        if (containsNonPrintableChars(txtValue)) {
+            return "0x" + toHexString(txtValue);
+        }
+
+        return new String(txtValue, StandardCharsets.US_ASCII);
+    }
+
+    /**
+     * Returns {@code true} if {@code txtValue} contains non-printable ASCII characters.
+     *
+     * The printable characters are in range of [32, 126].
+     */
+    private static boolean containsNonPrintableChars(byte[] txtValue) {
+        for (int i = 0; i < txtValue.length; i++) {
+            if (txtValue[i] < 32 || txtValue[i] > 126) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /** Implement the Parcelable interface */
     public int describeContents() {
         return 0;
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt
new file mode 100644
index 0000000..8f86f06
--- /dev/null
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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 android.net.nsd
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.ConnectivityModuleTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Unit tests for {@link NsdServiceInfo}. */
+@SmallTest
+@ConnectivityModuleTest
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
+class NsdServiceInfoTest {
+    @Test
+    fun testToString_txtRecord() {
+        val info = NsdServiceInfo().apply {
+            this.setAttribute("abc", byteArrayOf(0xff.toByte(), 0xfe.toByte()))
+            this.setAttribute("def", null as String?)
+            this.setAttribute("ghi", "猫")
+            this.setAttribute("jkl", byteArrayOf(0, 0x21))
+            this.setAttribute("mno", "Hey Tom! It's you?.~{}")
+        }
+
+        val infoStr = info.toString()
+
+        assertTrue(
+            infoStr.contains("txtRecord: " +
+                "{abc=0xFFFE, def=(null), ghi=0xE78CAB, jkl=0x0021, mno=Hey Tom! It's you?.~{}}"),
+            infoStr)
+    }
+}