Merge "Updates doTestAllowedUids to wait network Lost" into main
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index a0b2434..7b52694 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -62,6 +62,7 @@
     test_suites: [
         "cts",
         "general-tests",
-        "mts-tethering"
+        "mts-tethering",
+        "mcts-tethering",
     ],
 }
diff --git a/common/Android.bp b/common/Android.bp
index 1d73a46..f2f3929 100644
--- a/common/Android.bp
+++ b/common/Android.bp
@@ -19,12 +19,6 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-build = ["TrunkStable.bp"]
-
-// This is a placeholder comment to avoid merge conflicts
-// as the above target may not exist
-// depending on the branch
-
 // The library requires the final artifact to contain net-utils-device-common-struct.
 java_library {
     name: "connectivity-net-module-utils-bpf",
diff --git a/common/TrunkStable.bp b/common/TrunkStable.bp
deleted file mode 100644
index 59874c2..0000000
--- a/common/TrunkStable.bp
+++ /dev/null
@@ -1,16 +0,0 @@
-//
-// 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.
-//
-
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index 86745d4..60a88c0 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -127,6 +127,7 @@
 
   public final class IpSecTransform implements java.lang.AutoCloseable {
     method public void close();
+    method @FlaggedApi("com.android.net.flags.ipsec_transform_state") public void getIpSecTransformState(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.IpSecTransformState,java.lang.RuntimeException>);
   }
 
   public static class IpSecTransform.Builder {
@@ -138,6 +139,29 @@
     method @NonNull public android.net.IpSecTransform.Builder setIpv4Encapsulation(@NonNull android.net.IpSecManager.UdpEncapsulationSocket, int);
   }
 
+  @FlaggedApi("com.android.net.flags.ipsec_transform_state") public final class IpSecTransformState implements android.os.Parcelable {
+    method public int describeContents();
+    method public long getByteCount();
+    method public long getPacketCount();
+    method @NonNull public byte[] getReplayBitmap();
+    method public long getRxHighestSequenceNumber();
+    method public long getTimestamp();
+    method public long getTxHighestSequenceNumber();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.IpSecTransformState> CREATOR;
+  }
+
+  @FlaggedApi("com.android.net.flags.ipsec_transform_state") public static final class IpSecTransformState.Builder {
+    ctor public IpSecTransformState.Builder();
+    method @NonNull public android.net.IpSecTransformState build();
+    method @NonNull public android.net.IpSecTransformState.Builder setByteCount(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setPacketCount(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setReplayBitmap(@NonNull byte[]);
+    method @NonNull public android.net.IpSecTransformState.Builder setRxHighestSequenceNumber(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setTimestamp(long);
+    method @NonNull public android.net.IpSecTransformState.Builder setTxHighestSequenceNumber(long);
+  }
+
   public class TrafficStats {
     ctor public TrafficStats();
     method public static void clearThreadStatsTag();
@@ -251,6 +275,7 @@
     method public int getPort();
     method public String getServiceName();
     method public String getServiceType();
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") @NonNull public java.util.Set<java.lang.String> getSubtypes();
     method public void removeAttribute(String);
     method public void setAttribute(String, String);
     method @Deprecated public void setHost(java.net.InetAddress);
@@ -259,6 +284,7 @@
     method public void setPort(int);
     method public void setServiceName(String);
     method public void setServiceType(String);
+    method @FlaggedApi("com.android.net.flags.nsd_subtypes_support_enabled") public void setSubtypes(@NonNull java.util.Set<java.lang.String>);
     method public void writeToParcel(android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.nsd.NsdServiceInfo> CREATOR;
   }
diff --git a/framework-t/src/android/net/IIpSecService.aidl b/framework-t/src/android/net/IIpSecService.aidl
index 88ffd0e..f972ab9 100644
--- a/framework-t/src/android/net/IIpSecService.aidl
+++ b/framework-t/src/android/net/IIpSecService.aidl
@@ -22,6 +22,7 @@
 import android.net.IpSecUdpEncapResponse;
 import android.net.IpSecSpiResponse;
 import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -74,6 +75,8 @@
 
     void deleteTransform(int transformId);
 
+    IpSecTransformState getTransformState(int transformId);
+
     void applyTransportModeTransform(
             in ParcelFileDescriptor socket, int direction, int transformId);
 
diff --git a/framework-t/src/android/net/IpSecManager.java b/framework-t/src/android/net/IpSecManager.java
index 3afa6ef..3f74e1c 100644
--- a/framework-t/src/android/net/IpSecManager.java
+++ b/framework-t/src/android/net/IpSecManager.java
@@ -65,6 +65,13 @@
 public class IpSecManager {
     private static final String TAG = "IpSecManager";
 
+    // TODO : remove this class when udc-mainline-prod is abandoned and android.net.flags.Flags is
+    // available here
+    /** @hide */
+    public static class Flags {
+        static final String IPSEC_TRANSFORM_STATE = "com.android.net.flags.ipsec_transform_state";
+    }
+
     /**
      * Feature flag to declare the kernel support of updating IPsec SAs.
      *
@@ -1084,6 +1091,12 @@
         }
     }
 
+    /** @hide */
+    public IpSecTransformState getTransformState(int transformId)
+            throws IllegalStateException, RemoteException {
+        return mService.getTransformState(transformId);
+    }
+
     /**
      * Construct an instance of IpSecManager within an application context.
      *
diff --git a/framework-t/src/android/net/IpSecTransform.java b/framework-t/src/android/net/IpSecTransform.java
index c236b6c..246a2dd 100644
--- a/framework-t/src/android/net/IpSecTransform.java
+++ b/framework-t/src/android/net/IpSecTransform.java
@@ -15,8 +15,11 @@
  */
 package android.net;
 
+import static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
 import static android.net.IpSecManager.INVALID_RESOURCE_ID;
 
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -26,6 +29,8 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Binder;
+import android.os.OutcomeReceiver;
+import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.util.Log;
 
@@ -38,6 +43,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.net.InetAddress;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * This class represents a transform, which roughly corresponds to an IPsec Security Association.
@@ -201,6 +207,43 @@
     }
 
     /**
+     * Retrieve the current state of this IpSecTransform.
+     *
+     * @param executor The {@link Executor} on which to call the supplied callback.
+     * @param callback Callback that's called after the transform state is ready or when an error
+     *     occurs.
+     * @see IpSecTransformState
+     */
+    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    public void getIpSecTransformState(
+            @CallbackExecutor @NonNull Executor executor,
+            @NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
+
+        // TODO: Consider adding check to prevent DDoS attack.
+
+        try {
+            final IpSecTransformState ipSecTransformState =
+                    getIpSecManager(mContext).getTransformState(mResourceId);
+            executor.execute(
+                    () -> {
+                        callback.onResult(ipSecTransformState);
+                    });
+        } catch (IllegalStateException e) {
+            executor.execute(
+                    () -> {
+                        callback.onError(e);
+                    });
+        } catch (RemoteException e) {
+            executor.execute(
+                    () -> {
+                        callback.onError(e.rethrowFromSystemServer());
+                    });
+        }
+    }
+
+    /**
      * A callback class to provide status information regarding a NAT-T keepalive session
      *
      * <p>Use this callback to receive status information regarding a NAT-T keepalive session
diff --git a/framework-t/src/android/net/IpSecTransformState.aidl b/framework-t/src/android/net/IpSecTransformState.aidl
new file mode 100644
index 0000000..69cce28
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+/** @hide */
+parcelable IpSecTransformState;
\ No newline at end of file
diff --git a/framework-t/src/android/net/IpSecTransformState.java b/framework-t/src/android/net/IpSecTransformState.java
new file mode 100644
index 0000000..b575dd5
--- /dev/null
+++ b/framework-t/src/android/net/IpSecTransformState.java
@@ -0,0 +1,282 @@
+/*
+ * 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 static android.net.IpSecManager.Flags.IPSEC_TRANSFORM_STATE;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
+
+import java.util.Objects;
+
+/**
+ * This class represents a snapshot of the state of an IpSecTransform
+ *
+ * <p>This class provides the current state of an IpSecTransform, enabling link metric analysis by
+ * the caller. Use cases include understanding transform usage, such as packet and byte counts, as
+ * well as observing out-of-order delivery by checking the bitmap. Additionally, callers can query
+ * IpSecTransformStates at two timestamps. By comparing the changes in packet counts and sequence
+ * numbers, callers can estimate IPsec data loss in the inbound direction.
+ */
+@FlaggedApi(IPSEC_TRANSFORM_STATE)
+public final class IpSecTransformState implements Parcelable {
+    private final long mTimeStamp;
+    private final long mTxHighestSequenceNumber;
+    private final long mRxHighestSequenceNumber;
+    private final long mPacketCount;
+    private final long mByteCount;
+    private final byte[] mReplayBitmap;
+
+    private IpSecTransformState(
+            long timestamp,
+            long txHighestSequenceNumber,
+            long rxHighestSequenceNumber,
+            long packetCount,
+            long byteCount,
+            byte[] replayBitmap) {
+        mTimeStamp = timestamp;
+        mTxHighestSequenceNumber = txHighestSequenceNumber;
+        mRxHighestSequenceNumber = rxHighestSequenceNumber;
+        mPacketCount = packetCount;
+        mByteCount = byteCount;
+
+        Objects.requireNonNull(replayBitmap, "replayBitmap is null");
+        mReplayBitmap = replayBitmap.clone();
+
+        validate();
+    }
+
+    private void validate() {
+        Objects.requireNonNull(mReplayBitmap, "mReplayBitmap is null");
+    }
+
+    /**
+     * Deserializes a IpSecTransformState from a PersistableBundle.
+     *
+     * @hide
+     */
+    @VisibleForTesting(visibility = Visibility.PRIVATE)
+    public IpSecTransformState(@NonNull Parcel in) {
+        Objects.requireNonNull(in, "The input PersistableBundle is null");
+        mTimeStamp = in.readLong();
+        mTxHighestSequenceNumber = in.readLong();
+        mRxHighestSequenceNumber = in.readLong();
+        mPacketCount = in.readLong();
+        mByteCount = in.readLong();
+        mReplayBitmap = HexDump.hexStringToByteArray(in.readString());
+
+        validate();
+    }
+
+    // Parcelable methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel out, int flags) {
+        out.writeLong(mTimeStamp);
+        out.writeLong(mTxHighestSequenceNumber);
+        out.writeLong(mRxHighestSequenceNumber);
+        out.writeLong(mPacketCount);
+        out.writeLong(mByteCount);
+        out.writeString(HexDump.toHexString(mReplayBitmap));
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<IpSecTransformState> CREATOR =
+            new Parcelable.Creator<IpSecTransformState>() {
+                @NonNull
+                public IpSecTransformState createFromParcel(Parcel in) {
+                    return new IpSecTransformState(in);
+                }
+
+                @NonNull
+                public IpSecTransformState[] newArray(int size) {
+                    return new IpSecTransformState[size];
+                }
+            };
+
+    /**
+     * Retrieve the epoch timestamp (milliseconds) for when this state was created
+     *
+     * @see Builder#setTimestamp(long)
+     */
+    public long getTimestamp() {
+        return mTimeStamp;
+    }
+
+    /**
+     * Retrieve the highest sequence number sent so far
+     *
+     * @see Builder#setTxHighestSequenceNumber(long)
+     */
+    public long getTxHighestSequenceNumber() {
+        return mTxHighestSequenceNumber;
+    }
+
+    /**
+     * Retrieve the highest sequence number received so far
+     *
+     * @see Builder#setRxHighestSequenceNumber(long)
+     */
+    public long getRxHighestSequenceNumber() {
+        return mRxHighestSequenceNumber;
+    }
+
+    /**
+     * Retrieve the number of packets received AND sent so far
+     *
+     * @see Builder#setPacketCount(long)
+     */
+    public long getPacketCount() {
+        return mPacketCount;
+    }
+
+    /**
+     * Retrieve the number of bytes received AND sent so far
+     *
+     * @see Builder#setByteCount(long)
+     */
+    public long getByteCount() {
+        return mByteCount;
+    }
+
+    /**
+     * Retrieve the replay bitmap
+     *
+     * <p>This bitmap represents a replay window, allowing the caller to observe out-of-order
+     * delivery. The last bit represents the highest sequence number received so far and bits for
+     * the received packets will be marked as true.
+     *
+     * <p>The size of a replay bitmap will never change over the lifetime of an IpSecTransform
+     *
+     * <p>The replay bitmap is solely useful for inbound IpSecTransforms. For outbound
+     * IpSecTransforms, all bits will be unchecked.
+     *
+     * @see Builder#setReplayBitmap(byte[])
+     */
+    @NonNull
+    public byte[] getReplayBitmap() {
+        return mReplayBitmap.clone();
+    }
+
+    /** Builder class for testing purposes */
+    @FlaggedApi(IPSEC_TRANSFORM_STATE)
+    public static final class Builder {
+        private long mTimeStamp;
+        private long mTxHighestSequenceNumber;
+        private long mRxHighestSequenceNumber;
+        private long mPacketCount;
+        private long mByteCount;
+        private byte[] mReplayBitmap;
+
+        public Builder() {
+            mTimeStamp = System.currentTimeMillis();
+        }
+
+        /**
+         * Set the epoch timestamp (milliseconds) for when this state was created
+         *
+         * @see IpSecTransformState#getTimestamp()
+         */
+        @NonNull
+        public Builder setTimestamp(long timeStamp) {
+            mTimeStamp = timeStamp;
+            return this;
+        }
+
+        /**
+         * Set the highest sequence number sent so far
+         *
+         * @see IpSecTransformState#getTxHighestSequenceNumber()
+         */
+        @NonNull
+        public Builder setTxHighestSequenceNumber(long seqNum) {
+            mTxHighestSequenceNumber = seqNum;
+            return this;
+        }
+
+        /**
+         * Set the highest sequence number received so far
+         *
+         * @see IpSecTransformState#getRxHighestSequenceNumber()
+         */
+        @NonNull
+        public Builder setRxHighestSequenceNumber(long seqNum) {
+            mRxHighestSequenceNumber = seqNum;
+            return this;
+        }
+
+        /**
+         * Set the number of packets received AND sent so far
+         *
+         * @see IpSecTransformState#getPacketCount()
+         */
+        @NonNull
+        public Builder setPacketCount(long packetCount) {
+            mPacketCount = packetCount;
+            return this;
+        }
+
+        /**
+         * Set the number of bytes received AND sent so far
+         *
+         * @see IpSecTransformState#getByteCount()
+         */
+        @NonNull
+        public Builder setByteCount(long byteCount) {
+            mByteCount = byteCount;
+            return this;
+        }
+
+        /**
+         * Set the replay bitmap
+         *
+         * @see IpSecTransformState#getReplayBitmap()
+         */
+        @NonNull
+        public Builder setReplayBitmap(@NonNull byte[] bitMap) {
+            mReplayBitmap = bitMap.clone();
+            return this;
+        }
+
+        /**
+         * Build and validate the IpSecTransformState
+         *
+         * @return an immutable IpSecTransformState instance
+         */
+        @NonNull
+        public IpSecTransformState build() {
+            return new IpSecTransformState(
+                    mTimeStamp,
+                    mTxHighestSequenceNumber,
+                    mRxHighestSequenceNumber,
+                    mPacketCount,
+                    mByteCount,
+                    mReplayBitmap);
+        }
+    }
+}
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index bf01a9d..fcf79eb 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -150,6 +150,8 @@
     public static class Flags {
         static final String REGISTER_NSD_OFFLOAD_ENGINE_API =
                 "com.android.net.flags.register_nsd_offload_engine_api";
+        static final String NSD_SUBTYPES_SUPPORT_ENABLED =
+                "com.android.net.flags.nsd_subtypes_support_enabled";
     }
 
     /**
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index caeecdd..ac4ea23 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -16,6 +16,9 @@
 
 package android.net.nsd;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
@@ -24,6 +27,7 @@
 import android.os.Parcelable;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.net.module.util.InetAddressUtils;
@@ -35,6 +39,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A class representing service information for network service discovery
@@ -48,9 +53,11 @@
 
     private String mServiceType;
 
-    private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+    private final Set<String> mSubtypes;
 
-    private final List<InetAddress> mHostAddresses = new ArrayList<>();
+    private final ArrayMap<String, byte[]> mTxtRecord;
+
+    private final List<InetAddress> mHostAddresses;
 
     private int mPort;
 
@@ -60,14 +67,34 @@
     private int mInterfaceIndex;
 
     public NsdServiceInfo() {
+        mSubtypes = new ArraySet<>();
+        mTxtRecord = new ArrayMap<>();
+        mHostAddresses = new ArrayList<>();
     }
 
     /** @hide */
     public NsdServiceInfo(String sn, String rt) {
+        this();
         mServiceName = sn;
         mServiceType = rt;
     }
 
+    /**
+     * Creates a copy of {@code other}.
+     *
+     * @hide
+     */
+    public NsdServiceInfo(@NonNull NsdServiceInfo other) {
+        mServiceName = other.getServiceName();
+        mServiceType = other.getServiceType();
+        mSubtypes = new ArraySet<>(other.getSubtypes());
+        mTxtRecord = new ArrayMap<>(other.mTxtRecord);
+        mHostAddresses = new ArrayList<>(other.getHostAddresses());
+        mPort = other.getPort();
+        mNetwork = other.getNetwork();
+        mInterfaceIndex = other.getInterfaceIndex();
+    }
+
     /** Get the service name */
     public String getServiceName() {
         return mServiceName;
@@ -391,11 +418,41 @@
         mInterfaceIndex = interfaceIndex;
     }
 
+    /**
+     * Sets the subtypes to be advertised for this service instance.
+     *
+     * The elements in {@code subtypes} should be the subtype identifiers which have the trailing
+     * "._sub" removed. For example, the subtype should be "_printer" for
+     * "_printer._sub._http._tcp.local".
+     *
+     * Only one subtype will be registered if multiple elements of {@code subtypes} have the same
+     * case-insensitive value.
+     */
+    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    public void setSubtypes(@NonNull Set<String> subtypes) {
+        mSubtypes.clear();
+        mSubtypes.addAll(subtypes);
+    }
+
+    /**
+     * Returns subtypes of this service instance.
+     *
+     * When this object is returned by the service discovery/browse APIs (etc. {@link
+     * NsdManager.DiscoveryListener}), the return value may or may not include the subtypes of this
+     * service.
+     */
+    @FlaggedApi(NsdManager.Flags.NSD_SUBTYPES_SUPPORT_ENABLED)
+    @NonNull
+    public Set<String> getSubtypes() {
+        return Collections.unmodifiableSet(mSubtypes);
+    }
+
     @Override
     public String toString() {
         StringBuilder sb = new StringBuilder();
         sb.append("name: ").append(mServiceName)
                 .append(", type: ").append(mServiceType)
+                .append(", subtypes: ").append(TextUtils.join(", ", mSubtypes))
                 .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
                 .append(", port: ").append(mPort)
                 .append(", network: ").append(mNetwork);
@@ -414,6 +471,7 @@
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeString(mServiceName);
         dest.writeString(mServiceType);
+        dest.writeStringList(new ArrayList<>(mSubtypes));
         dest.writeInt(mPort);
 
         // TXT record key/value pairs.
@@ -445,6 +503,7 @@
                 NsdServiceInfo info = new NsdServiceInfo();
                 info.mServiceName = in.readString();
                 info.mServiceType = in.readString();
+                info.setSubtypes(new ArraySet<>(in.createStringArrayList()));
                 info.mPort = in.readInt();
 
                 // TXT record key/value pairs.
diff --git a/framework/src/android/net/NetworkRequest.java b/framework/src/android/net/NetworkRequest.java
index 9824faa..653e41d 100644
--- a/framework/src/android/net/NetworkRequest.java
+++ b/framework/src/android/net/NetworkRequest.java
@@ -290,7 +290,10 @@
          * Therefore these capabilities are only in NetworkRequest.
          */
         private static final int[] DEFAULT_FORBIDDEN_CAPABILITIES = new int[] {
-            NET_CAPABILITY_LOCAL_NETWORK
+            // TODO(b/313030307): this should contain NET_CAPABILITY_LOCAL_NETWORK.
+            // We cannot currently add it because doing so would crash if the module rolls back,
+            // because JobScheduler persists NetworkRequests to disk, and existing production code
+            // does not consider LOCAL_NETWORK to be a valid capability.
         };
 
         private final NetworkCapabilities mNetworkCapabilities;
diff --git a/nearby/README.md b/nearby/README.md
index 2731b3e..0d26563 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -49,6 +49,7 @@
 ```sh
 Build unbundled module using banchan
 
+$ source build/envsetup.sh
 $ banchan com.google.android.tethering mainline_modules_arm64
 $ m apps_only dist
 $ adb install out/dist/com.google.android.tethering.apex
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 070a2b6..00f1c38 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -284,6 +284,8 @@
      */
     public void queryOffloadCapability(@NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<OffloadCapability> callback) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(callback);
         try {
             mService.queryOffloadCapability(new OffloadTransport(executor, callback));
         } catch (RemoteException e) {
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 9b32d69..9ef905d 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -54,6 +54,11 @@
     public static final String NEARBY_REFACTOR_DISCOVERY_MANAGER =
             "nearby_refactor_discovery_manager";
 
+    /**
+     * Flag to guard enable BLE during Nearby Service init time.
+     */
+    public static final String NEARBY_ENABLE_BLE_IN_INIT = "nearby_enable_ble_in_init";
+
     private static final boolean IS_USER_BUILD = "user".equals(Build.TYPE);
 
     private final DeviceConfigListener mDeviceConfigListener = new DeviceConfigListener();
@@ -67,6 +72,8 @@
     private boolean mSupportTestApp;
     @GuardedBy("mDeviceConfigLock")
     private boolean mRefactorDiscoveryManager;
+    @GuardedBy("mDeviceConfigLock")
+    private boolean mEnableBleInInit;
 
     public NearbyConfiguration() {
         mDeviceConfigListener.start();
@@ -131,6 +138,15 @@
         }
     }
 
+    /**
+     * @return {@code true} if enableBLE() is called during NearbyService init time.
+     */
+    public boolean enableBleInInit() {
+        synchronized (mDeviceConfigLock) {
+            return mEnableBleInInit;
+        }
+    }
+
     private class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
         public void start() {
             DeviceConfig.addOnPropertiesChangedListener(getNamespace(),
@@ -149,6 +165,8 @@
                         NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
                 mRefactorDiscoveryManager = getDeviceConfigBoolean(
                         NEARBY_REFACTOR_DISCOVERY_MANAGER, false /* defaultValue */);
+                mEnableBleInInit = getDeviceConfigBoolean(
+                        NEARBY_ENABLE_BLE_IN_INIT, true /* defaultValue */);
             }
         }
     }
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
index 0c41426..8995232 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManager.java
@@ -21,6 +21,7 @@
 import static com.android.server.nearby.NearbyService.TAG;
 
 import android.annotation.Nullable;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -35,6 +36,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.managers.registration.DiscoveryRegistration;
 import com.android.server.nearby.provider.AbstractDiscoveryProvider;
@@ -66,6 +68,7 @@
     private final BleDiscoveryProvider mBleDiscoveryProvider;
     private final Injector mInjector;
     private final Executor mExecutor;
+    private final NearbyConfiguration mNearbyConfiguration;
 
     public DiscoveryProviderManager(Context context, Injector injector) {
         Log.v(TAG, "DiscoveryProviderManager: ");
@@ -75,6 +78,7 @@
         mChreDiscoveryProvider = new ChreDiscoveryProvider(mContext,
                 new ChreCommunication(injector, mContext, mExecutor), mExecutor);
         mInjector = injector;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     @VisibleForTesting
@@ -86,6 +90,7 @@
         mInjector = injector;
         mBleDiscoveryProvider = bleDiscoveryProvider;
         mChreDiscoveryProvider = chreDiscoveryProvider;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     private static boolean isChreOnly(Set<ScanFilter> scanFilters) {
@@ -141,6 +146,10 @@
 
     /** Called after boot completed. */
     public void init() {
+        // Register BLE only scan when Bluetooth is turned off
+        if (mNearbyConfiguration.enableBleInInit()) {
+            setBleScanEnabled();
+        }
         if (mInjector.getContextHubManager() != null) {
             mChreDiscoveryProvider.init();
         }
@@ -242,7 +251,7 @@
     @GuardedBy("mMultiplexerLock")
     private void startBleProvider(Set<ScanFilter> scanFilters) {
         if (!mBleDiscoveryProvider.getController().isStarted()) {
-            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            Log.d(TAG, "DiscoveryProviderManager starts BLE scanning.");
             mBleDiscoveryProvider.getController().setListener(this);
             mBleDiscoveryProvider.getController().setProviderScanMode(mMerged.getScanMode());
             mBleDiscoveryProvider.getController().setProviderScanFilters(
@@ -313,4 +322,29 @@
     public void onMergedRegistrationsUpdated() {
         invalidateProviderScanMode();
     }
+
+    /**
+     * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+     * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+     * registers Nearby service to Ble scan when Blutooth is off.
+     */
+    public boolean setBleScanEnabled() {
+        BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+        if (adapter == null) {
+            Log.e(TAG, "BluetoothAdapter is null.");
+            return false;
+        }
+        if (adapter.isEnabled() || adapter.isLeEnabled()) {
+            return true;
+        }
+        if (!adapter.isBleScanAlwaysAvailable()) {
+            Log.v(TAG, "Ble always on scan is disabled.");
+            return false;
+        }
+        if (!adapter.enableBLE()) {
+            Log.e(TAG, "Failed to register Ble scan.");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
index 4b76eba..3ef8fb7 100644
--- a/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
+++ b/nearby/service/java/com/android/server/nearby/managers/DiscoveryProviderManagerLegacy.java
@@ -22,6 +22,7 @@
 
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -38,6 +39,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.metrics.NearbyMetrics;
 import com.android.server.nearby.presence.PresenceDiscoveryResult;
@@ -69,6 +71,7 @@
     private final Context mContext;
     private final BleDiscoveryProvider mBleDiscoveryProvider;
     private final Injector mInjector;
+    private final NearbyConfiguration mNearbyConfiguration;
     @ScanRequest.ScanMode
     private int mScanMode;
     @GuardedBy("mLock")
@@ -83,6 +86,7 @@
                         mContext, new ChreCommunication(injector, mContext, executor), executor);
         mScanTypeScanListenerRecordMap = new HashMap<>();
         mInjector = injector;
+        mNearbyConfiguration = new NearbyConfiguration();
         Log.v(TAG, "DiscoveryProviderManagerLegacy: ");
     }
 
@@ -96,6 +100,7 @@
         mBleDiscoveryProvider = bleDiscoveryProvider;
         mChreDiscoveryProvider = chreDiscoveryProvider;
         mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+        mNearbyConfiguration = new NearbyConfiguration();
     }
 
     private static boolean isChreOnly(List<ScanFilter> scanFilters) {
@@ -142,18 +147,18 @@
             for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
                 ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
                 if (record == null) {
-                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
                     continue;
                 }
                 CallerIdentity callerIdentity = record.getCallerIdentity();
                 if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
                         appOpsManager, callerIdentity)) {
-                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                    Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
                             + "- not forwarding results");
                     try {
                         record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
                     } catch (RemoteException e) {
-                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                        Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
                     }
                     return;
                 }
@@ -180,7 +185,7 @@
                     NearbyMetrics.logScanDeviceDiscovered(
                             record.hashCode(), record.getScanRequest(), nearbyDevice);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "DiscoveryProviderManager failed to report onDiscovered.", e);
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onDiscovered.", e);
                 }
             }
         }
@@ -193,18 +198,18 @@
             for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
                 ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
                 if (record == null) {
-                    Log.w(TAG, "DiscoveryProviderManager cannot find the scan record.");
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy cannot find the scan record.");
                     continue;
                 }
                 CallerIdentity callerIdentity = record.getCallerIdentity();
                 if (!DiscoveryPermissions.noteDiscoveryResultDelivery(
                         appOpsManager, callerIdentity)) {
-                    Log.w(TAG, "[DiscoveryProviderManager] scan permission revoked "
+                    Log.w(TAG, "[DiscoveryProviderManagerLegacy] scan permission revoked "
                             + "- not forwarding results");
                     try {
                         record.getScanListener().onError(ScanCallback.ERROR_PERMISSION_DENIED);
                     } catch (RemoteException e) {
-                        Log.w(TAG, "DiscoveryProviderManager failed to report error.", e);
+                        Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report error.", e);
                     }
                     return;
                 }
@@ -212,7 +217,7 @@
                 try {
                     record.getScanListener().onError(errorCode);
                 } catch (RemoteException e) {
-                    Log.w(TAG, "DiscoveryProviderManager failed to report onError.", e);
+                    Log.w(TAG, "DiscoveryProviderManagerLegacy failed to report onError.", e);
                 }
             }
         }
@@ -220,6 +225,10 @@
 
     /** Called after boot completed. */
     public void init() {
+        // Register BLE only scan when Bluetooth is turned off
+        if (mNearbyConfiguration.enableBleInInit()) {
+            setBleScanEnabled();
+        }
         if (mInjector.getContextHubManager() != null) {
             mChreDiscoveryProvider.init();
         }
@@ -293,10 +302,10 @@
             if (listenerBinder != null && deathRecipient != null) {
                 listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
             }
-            Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
+            Log.v(TAG, "DiscoveryProviderManagerLegacy unregistered scan listener.");
             NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
             if (mScanTypeScanListenerRecordMap.isEmpty()) {
-                Log.v(TAG, "DiscoveryProviderManager stops provider because there is no "
+                Log.v(TAG, "DiscoveryProviderManagerLegacy stops provider because there is no "
                         + "scan listener registered.");
                 stopProviders();
                 return;
@@ -306,8 +315,8 @@
 
             // Removes current highest scan mode requested and sets the next highest scan mode.
             if (removedRecord.getScanRequest().getScanMode() == mScanMode) {
-                Log.v(TAG, "DiscoveryProviderManager starts to find the new highest scan mode "
-                        + "because the highest scan mode listener was unregistered.");
+                Log.v(TAG, "DiscoveryProviderManagerLegacy starts to find the new highest "
+                        + "scan mode because the highest scan mode listener was unregistered.");
                 @ScanRequest.ScanMode int highestScanModeRequested = ScanRequest.SCAN_MODE_NO_POWER;
                 // find the next highest scan mode;
                 for (ScanListenerRecord record : mScanTypeScanListenerRecordMap.values()) {
@@ -377,7 +386,7 @@
 
     private void startBleProvider(List<ScanFilter> scanFilters) {
         if (!mBleDiscoveryProvider.getController().isStarted()) {
-            Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
+            Log.d(TAG, "DiscoveryProviderManagerLegacy starts BLE scanning.");
             mBleDiscoveryProvider.getController().setListener(this);
             mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
             mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
@@ -387,7 +396,7 @@
 
     @VisibleForTesting
     void startChreProvider(List<ScanFilter> scanFilters) {
-        Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+        Log.d(TAG, "DiscoveryProviderManagerLegacy starts CHRE scanning.");
         mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
         mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
         mChreDiscoveryProvider.getController().start();
@@ -503,4 +512,29 @@
             unregisterScanListener(listener);
         }
     }
+
+    /**
+     * Registers Nearby service to Ble scan if Bluetooth is off. (Even when Bluetooth is off)
+     * @return {@code true} when Nearby currently can scan through Bluetooth or Ble or successfully
+     * registers Nearby service to Ble scan when Blutooth is off.
+     */
+    public boolean setBleScanEnabled() {
+        BluetoothAdapter adapter = mInjector.getBluetoothAdapter();
+        if (adapter == null) {
+            Log.e(TAG, "BluetoothAdapter is null.");
+            return false;
+        }
+        if (adapter.isEnabled() || adapter.isLeEnabled()) {
+            return true;
+        }
+        if (!adapter.isBleScanAlwaysAvailable()) {
+            Log.v(TAG, "Ble always on scan is disabled.");
+            return false;
+        }
+        if (!adapter.enableBLE()) {
+            Log.e(TAG, "Failed to register Ble scan.");
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
index 1f9f70b..ac1e18f 100644
--- a/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
+++ b/nearby/service/java/com/android/server/nearby/presence/EncryptionInfo.java
@@ -17,11 +17,13 @@
 package com.android.server.nearby.presence;
 
 import android.annotation.IntDef;
+import android.annotation.Nullable;
 import android.nearby.DataElement;
 
 import androidx.annotation.NonNull;
 
 import com.android.internal.util.Preconditions;
+import com.android.server.nearby.util.ArrayUtils;
 
 import java.util.Arrays;
 
@@ -40,7 +42,7 @@
 
     // 1st byte : encryption scheme
     // 2nd to 17th bytes: salt
-    private static final int ENCRYPTION_INFO_LENGTH = 17;
+    public static final int ENCRYPTION_INFO_LENGTH = 17;
     private static final int ENCODING_SCHEME_MASK = 0b01111000;
     private static final int ENCODING_SCHEME_OFFSET = 3;
     @EncodingScheme
@@ -69,4 +71,20 @@
     public byte[] getSalt() {
         return mSalt;
     }
+
+    /** Combines the encoding scheme and salt to a byte array
+     * that represents an {@link EncryptionInfo}.
+     */
+    @Nullable
+    public static byte[] toByte(@EncodingScheme int encodingScheme, byte[] salt) {
+        if (ArrayUtils.isEmpty(salt)) {
+            return null;
+        }
+        if (salt.length != ENCRYPTION_INFO_LENGTH - 1) {
+            return null;
+        }
+        byte schemeByte =
+                (byte) ((encodingScheme << ENCODING_SCHEME_OFFSET) & ENCODING_SCHEME_MASK);
+        return ArrayUtils.append(schemeByte, salt);
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
index 34a7514..c2304cc 100644
--- a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -16,22 +16,27 @@
 
 package com.android.server.nearby.presence;
 
+import static android.nearby.BroadcastRequest.PRESENCE_VERSION_V1;
+
 import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.EncryptionInfo.ENCRYPTION_INFO_LENGTH;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
 
 import android.annotation.Nullable;
-import android.nearby.BroadcastRequest;
+import android.nearby.BroadcastRequest.BroadcastVersion;
 import android.nearby.DataElement;
+import android.nearby.DataElement.DataType;
 import android.nearby.PresenceBroadcastRequest;
 import android.nearby.PresenceCredential;
 import android.nearby.PublicCredential;
 import android.util.Log;
 
+import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.encryption.Cryptor;
-import com.android.server.nearby.util.encryption.CryptorImpFake;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
 
 import java.nio.ByteBuffer;
+import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -51,37 +56,51 @@
  * The header contains:
  * version (3 bits) | 5 bit reserved for future use (RFU)
  */
-public class ExtendedAdvertisement extends Advertisement{
+public class ExtendedAdvertisement extends Advertisement {
 
     public static final int SALT_DATA_LENGTH = 2;
-
     static final int HEADER_LENGTH = 1;
 
     static final int IDENTITY_DATA_LENGTH = 16;
-
+    // Identity Index is always 2 .
+    // 0 is reserved, 1 is Salt or Credential Element.
+    private static final int CIPHER_START_INDEX = 2;
     private final List<DataElement> mDataElements;
+    private final byte[] mKeySeed;
 
-    private final byte[] mAuthenticityKey;
+    private final byte[] mData;
 
-    // All Data Elements including salt and identity.
-    // Each list item (byte array) is a Data Element (with its header).
-    private final List<byte[]> mCompleteDataElementsBytes;
-    // Signature generated from data elements.
-    private final byte[] mHmacTag;
+    private ExtendedAdvertisement(
+            @PresenceCredential.IdentityType int identityType,
+            byte[] identity,
+            byte[] salt,
+            byte[] keySeed,
+            List<Integer> actions,
+            List<DataElement> dataElements) {
+        this.mVersion = PRESENCE_VERSION_V1;
+        this.mIdentityType = identityType;
+        this.mIdentity = identity;
+        this.mSalt = salt;
+        this.mKeySeed = keySeed;
+        this.mDataElements = dataElements;
+        this.mActions = actions;
+        mData = toBytesInternal();
+    }
 
     /**
      * Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+     *
      * @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
      */
     @Nullable
     public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
-        if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+        if (request.getVersion() != PRESENCE_VERSION_V1) {
             Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
             return null;
         }
 
         byte[] salt = request.getSalt();
-        if (salt.length != SALT_DATA_LENGTH) {
+        if (salt.length != SALT_DATA_LENGTH && salt.length != ENCRYPTION_INFO_LENGTH - 1) {
             Log.v(TAG, "Salt does not match correct length");
             return null;
         }
@@ -94,12 +113,12 @@
         }
 
         List<Integer> actions = request.getActions();
-        if (actions.isEmpty()) {
-            Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
-            return null;
-        }
-
         List<DataElement> dataElements = request.getExtendedProperties();
+        // DataElements should include actions.
+        for (int action : actions) {
+            dataElements.add(
+                    new DataElement(DataType.ACTION, new byte[]{(byte) action}));
+        }
         return new ExtendedAdvertisement(
                 request.getCredential().getIdentityType(),
                 identity,
@@ -109,149 +128,252 @@
                 dataElements);
     }
 
-    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
-    @Nullable
-    public byte[] toBytes() {
-        ByteBuffer buffer = ByteBuffer.allocate(getLength());
-
-        // Header
-        buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
-
-        // Salt
-        buffer.put(mCompleteDataElementsBytes.get(0));
-
-        // Identity
-        buffer.put(mCompleteDataElementsBytes.get(1));
-
-        List<Byte> rawDataBytes = new ArrayList<>();
-        // Data Elements (Already includes salt and identity)
-        for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
-            byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
-            for (Byte b : dataElementBytes) {
-                rawDataBytes.add(b);
-            }
-        }
-
-        byte[] dataElements = new byte[rawDataBytes.size()];
-        for (int i = 0; i < rawDataBytes.size(); i++) {
-            dataElements[i] = rawDataBytes.get(i);
-        }
-
-        buffer.put(
-                getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
-
-        buffer.put(mHmacTag);
-
-        return buffer.array();
-    }
-
-    /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
-     * {@code null} when there is something when parsing.
+    /**
+     * Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+     * Return {@code null} when there is an error in parsing.
      */
     @Nullable
-    public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
-        @BroadcastRequest.BroadcastVersion
+    public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential sharedCredential) {
+        @BroadcastVersion
         int version = ExtendedAdvertisementUtils.getVersion(bytes);
-        if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+        if (version != PRESENCE_VERSION_V1) {
             Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
             return null;
         }
 
-        byte[] authenticityKey = publicCredential.getAuthenticityKey();
-
-        int index = HEADER_LENGTH;
-        // Salt
-        byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
-        DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
-        if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
-            Log.v(TAG, "First data element has to be salt.");
+        byte[] keySeed = sharedCredential.getAuthenticityKey();
+        byte[] metadataEncryptionKeyUnsignedAdvTag = sharedCredential.getEncryptedMetadataKeyTag();
+        if (keySeed == null || metadataEncryptionKeyUnsignedAdvTag == null) {
             return null;
         }
-        index += saltHeaderArray.length;
-        byte[] salt = new byte[saltHeader.getDataLength()];
-        for (int i = 0; i < saltHeader.getDataLength(); i++) {
-            salt[i] = bytes[index++];
-        }
 
-        // Identity
+        int index = 0;
+        // Header
+        byte[] header = new byte[]{bytes[index]};
+        index += HEADER_LENGTH;
+        // Section header
+        byte[] sectionHeader = new byte[]{bytes[index]};
+        index += HEADER_LENGTH;
+        // Salt or Encryption Info
+        byte[] firstHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+        DataElementHeader firstHeader = DataElementHeader.fromBytes(version, firstHeaderArray);
+        if (firstHeader == null) {
+            Log.v(TAG, "Cannot find salt.");
+            return null;
+        }
+        @DataType int firstType = firstHeader.getDataType();
+        if (firstType != DataType.SALT && firstType != DataType.ENCRYPTION_INFO) {
+            Log.v(TAG, "First data element has to be Salt or Encryption Info.");
+            return null;
+        }
+        index += firstHeaderArray.length;
+        byte[] firstDeBytes = new byte[firstHeader.getDataLength()];
+        for (int i = 0; i < firstHeader.getDataLength(); i++) {
+            firstDeBytes[i] = bytes[index++];
+        }
+        byte[] nonce = getNonce(firstType, firstDeBytes);
+        if (nonce == null) {
+            return null;
+        }
+        byte[] saltBytes = firstType == DataType.SALT
+                ? firstDeBytes : (new EncryptionInfo(firstDeBytes)).getSalt();
+
+        // Identity header
         byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
         DataElementHeader identityHeader =
                 DataElementHeader.fromBytes(version, identityHeaderArray);
-        if (identityHeader == null) {
-            Log.v(TAG, "The second element has to be identity.");
+        if (identityHeader == null || identityHeader.getDataLength() != IDENTITY_DATA_LENGTH) {
+            Log.v(TAG, "The second element has to be a 16-bytes identity.");
             return null;
         }
         index += identityHeaderArray.length;
         @PresenceCredential.IdentityType int identityType =
                 toPresenceCredentialIdentityType(identityHeader.getDataType());
-        if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
-            Log.v(TAG, "The identity type is unknown.");
+        if (identityType != PresenceCredential.IDENTITY_TYPE_PRIVATE
+                && identityType != PresenceCredential.IDENTITY_TYPE_TRUSTED) {
+            Log.v(TAG, "Only supports encrypted advertisement.");
             return null;
         }
-        byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
-        for (int i = 0; i < identityHeader.getDataLength(); i++) {
-            encryptedIdentity[i] = bytes[index++];
-        }
-        byte[] identity =
-                CryptorImpIdentityV1
-                        .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
-
-        Cryptor cryptor = getCryptor(/* encrypt= */ true);
-        byte[] encryptedDataElements =
-                new byte[bytes.length - index - cryptor.getSignatureLength()];
-        // Decrypt other data elements
-        System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
-        byte[] decryptedDataElements =
-                cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
-        if (decryptedDataElements == null) {
+        // Ciphertext
+        Cryptor cryptor = CryptorMicImp.getInstance();
+        byte[] ciphertext = new byte[bytes.length - index - cryptor.getSignatureLength()];
+        System.arraycopy(bytes, index, ciphertext, 0, ciphertext.length);
+        byte[] plaintext = cryptor.decrypt(ciphertext, nonce, keySeed);
+        if (plaintext == null) {
             return null;
         }
 
+        // Verification
+        // Verify the computed metadata encryption key tag
+        // First 16 bytes is metadata encryption key data
+        byte[] metadataEncryptionKey = new byte[IDENTITY_DATA_LENGTH];
+        System.arraycopy(plaintext, 0, metadataEncryptionKey, 0, IDENTITY_DATA_LENGTH);
+        // Verify metadata encryption key tag
+        byte[] computedMetadataEncryptionKeyTag =
+                CryptorMicImp.generateMetadataEncryptionKeyTag(metadataEncryptionKey,
+                        keySeed);
+        if (!Arrays.equals(computedMetadataEncryptionKeyTag, metadataEncryptionKeyUnsignedAdvTag)) {
+            Log.w(TAG,
+                    "The calculated metadata encryption key tag is different from the metadata "
+                            + "encryption key unsigned adv tag in the SharedCredential.");
+            return null;
+        }
         // Verify the computed HMAC tag is equal to HMAC tag in advertisement
-        if (cryptor.getSignatureLength() > 0) {
-            byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
-            System.arraycopy(
-                    bytes, bytes.length - cryptor.getSignatureLength(),
-                    expectedHmacTag, 0, cryptor.getSignatureLength());
-            if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
-                Log.e(TAG, "HMAC tags not match.");
-                return null;
-            }
+        byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+        System.arraycopy(
+                bytes, bytes.length - cryptor.getSignatureLength(),
+                expectedHmacTag, 0, cryptor.getSignatureLength());
+        byte[] micInput =  ArrayUtils.concatByteArrays(
+                PRESENCE_UUID_BYTES, header, sectionHeader,
+                firstHeaderArray, firstDeBytes,
+                nonce, identityHeaderArray, ciphertext);
+        if (!cryptor.verify(micInput, keySeed, expectedHmacTag)) {
+            Log.e(TAG, "HMAC tag not match.");
+            return null;
         }
 
-        int dataElementArrayIndex = 0;
-        // Other Data Elements
-        List<Integer> actions = new ArrayList<>();
-        List<DataElement> dataElements = new ArrayList<>();
-        while (dataElementArrayIndex < decryptedDataElements.length) {
-            byte[] deHeaderArray = ExtendedAdvertisementUtils
-                    .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
-            DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
-            dataElementArrayIndex += deHeaderArray.length;
+        byte[] otherDataElements = new byte[plaintext.length - IDENTITY_DATA_LENGTH];
+        System.arraycopy(plaintext, IDENTITY_DATA_LENGTH,
+                otherDataElements, 0, otherDataElements.length);
+        List<DataElement> dataElements = getDataElementsFromBytes(version, otherDataElements);
+        if (dataElements.isEmpty()) {
+            return null;
+        }
+        List<Integer> actions = getActionsFromDataElements(dataElements);
+        if (actions == null) {
+            return null;
+        }
+        return new ExtendedAdvertisement(identityType, metadataEncryptionKey, saltBytes, keySeed,
+                actions, dataElements);
+    }
 
-            @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
-            if (type == DataElement.DataType.ACTION) {
-                if (deHeader.getDataLength() != 1) {
-                    Log.v(TAG, "Action id should only 1 byte.");
+    @PresenceCredential.IdentityType
+    private static int toPresenceCredentialIdentityType(@DataType int type) {
+        switch (type) {
+            case DataType.PRIVATE_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+            case DataType.PROVISIONED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+            case DataType.TRUSTED_IDENTITY:
+                return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+            case DataType.PUBLIC_IDENTITY:
+            default:
+                return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+        }
+    }
+
+    @DataType
+    private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+        switch (identityType) {
+            case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+                return DataType.PRIVATE_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+                return DataType.PROVISIONED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+                return DataType.TRUSTED_IDENTITY;
+            case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+            default:
+                return DataType.PUBLIC_IDENTITY;
+        }
+    }
+
+    /**
+     * Returns {@code true} if the given {@link DataType} is salt, or one of the
+     * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+     */
+    private static boolean isSaltOrIdentity(@DataType int type) {
+        return type == DataType.SALT || type == DataType.ENCRYPTION_INFO
+                || type == DataType.PRIVATE_IDENTITY
+                || type == DataType.TRUSTED_IDENTITY
+                || type == DataType.PROVISIONED_IDENTITY
+                || type == DataType.PUBLIC_IDENTITY;
+    }
+
+    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+    @Nullable
+    public byte[] toBytes() {
+        return mData.clone();
+    }
+
+    /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+    @Nullable
+    public byte[] toBytesInternal() {
+        int sectionLength = 0;
+        // Salt
+        DataElement saltDe;
+        byte[] nonce;
+        try {
+            switch (mSalt.length) {
+                case SALT_DATA_LENGTH:
+                    saltDe = new DataElement(DataType.SALT, mSalt);
+                    nonce = CryptorMicImp.generateAdvNonce(mSalt);
+                    break;
+                case ENCRYPTION_INFO_LENGTH - 1:
+                    saltDe = new DataElement(DataType.ENCRYPTION_INFO,
+                            EncryptionInfo.toByte(EncryptionInfo.EncodingScheme.MIC, mSalt));
+                    nonce = CryptorMicImp.generateAdvNonce(mSalt, CIPHER_START_INDEX);
+                    break;
+                default:
+                    Log.w(TAG, "Invalid salt size.");
                     return null;
-                }
-                actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
-            } else {
-                if (isSaltOrIdentity(type)) {
-                    Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
-                            + " and one identity in the advertisement.");
-                    return null;
-                }
-                byte[] deData = new byte[deHeader.getDataLength()];
-                for (int i = 0; i < deHeader.getDataLength(); i++) {
-                    deData[i] = decryptedDataElements[dataElementArrayIndex++];
-                }
-                dataElements.add(new DataElement(type, deData));
             }
+        } catch (GeneralSecurityException e) {
+            Log.w(TAG, "Failed to generate the IV for encryption.", e);
+            return null;
         }
 
-        return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
-                dataElements);
+        byte[] saltOrEncryptionInfoBytes =
+                ExtendedAdvertisementUtils.convertDataElementToBytes(saltDe);
+        sectionLength += saltOrEncryptionInfoBytes.length;
+        // 16 bytes encrypted identity
+        @DataType int identityDataType = toDataType(getIdentityType());
+        byte[] identityHeaderBytes = new DataElementHeader(PRESENCE_VERSION_V1,
+                identityDataType, mIdentity.length).toBytes();
+        sectionLength += identityHeaderBytes.length;
+        final List<DataElement> dataElementList = getDataElements();
+        byte[] ciphertext = getCiphertext(nonce, dataElementList);
+        if (ciphertext == null) {
+            return null;
+        }
+        sectionLength += ciphertext.length;
+        // mic
+        sectionLength += CryptorMicImp.MIC_LENGTH;
+        mLength = sectionLength;
+        // header
+        byte header = ExtendedAdvertisementUtils.constructHeader(getVersion());
+        mLength += HEADER_LENGTH;
+        // section header
+        if (sectionLength > 255) {
+            Log.e(TAG, "A section should be shorter than 255 bytes.");
+            return null;
+        }
+        byte sectionHeader = (byte) sectionLength;
+        mLength += HEADER_LENGTH;
+
+        // generates mic
+        ByteBuffer micInputBuffer = ByteBuffer.allocate(
+                mLength + PRESENCE_UUID_BYTES.length + nonce.length - CryptorMicImp.MIC_LENGTH);
+        micInputBuffer.put(PRESENCE_UUID_BYTES);
+        micInputBuffer.put(header);
+        micInputBuffer.put(sectionHeader);
+        micInputBuffer.put(saltOrEncryptionInfoBytes);
+        micInputBuffer.put(nonce);
+        micInputBuffer.put(identityHeaderBytes);
+        micInputBuffer.put(ciphertext);
+        byte[] micInput = micInputBuffer.array();
+        byte[] mic = CryptorMicImp.getInstance().sign(micInput, mKeySeed);
+        if (mic == null) {
+            return null;
+        }
+
+        ByteBuffer buffer = ByteBuffer.allocate(mLength);
+        buffer.put(header);
+        buffer.put(sectionHeader);
+        buffer.put(saltOrEncryptionInfoBytes);
+        buffer.put(identityHeaderBytes);
+        buffer.put(ciphertext);
+        buffer.put(mic);
+        return buffer.array();
     }
 
     /** Returns the {@link DataElement}s in the advertisement. */
@@ -260,7 +382,7 @@
     }
 
     /** Returns the {@link DataElement}s in the advertisement according to the key. */
-    public List<DataElement> getDataElements(@DataElement.DataType int key) {
+    public List<DataElement> getDataElements(@DataType int key) {
         List<DataElement> res = new ArrayList<>();
         for (DataElement dataElement : mDataElements) {
             if (key == dataElement.getKey()) {
@@ -285,125 +407,86 @@
                 getActions());
     }
 
-    ExtendedAdvertisement(
-            @PresenceCredential.IdentityType int identityType,
-            byte[] identity,
-            byte[] salt,
-            byte[] authenticityKey,
-            List<Integer> actions,
-            List<DataElement> dataElements) {
-        this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
-        this.mIdentityType = identityType;
-        this.mIdentity = identity;
-        this.mSalt = salt;
-        this.mAuthenticityKey = authenticityKey;
-        this.mActions = actions;
-        this.mDataElements = dataElements;
-        this.mCompleteDataElementsBytes = new ArrayList<>();
+    @Nullable
+    private byte[] getCiphertext(byte[] nonce, List<DataElement> dataElements) {
+        Cryptor cryptor = CryptorMicImp.getInstance();
+        byte[] rawDeBytes = mIdentity;
+        for (DataElement dataElement : dataElements) {
+            rawDeBytes = ArrayUtils.concatByteArrays(rawDeBytes,
+                    ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement));
+        }
+        return cryptor.encrypt(rawDeBytes, nonce, mKeySeed);
+    }
 
-        int length = HEADER_LENGTH; // header
+    private static List<DataElement> getDataElementsFromBytes(
+            @BroadcastVersion int version, byte[] bytes) {
+        List<DataElement> res = new ArrayList<>();
+        if (ArrayUtils.isEmpty(bytes)) {
+            return res;
+        }
+        int index = 0;
+        while (index < bytes.length) {
+            byte[] deHeaderArray = ExtendedAdvertisementUtils
+                    .getDataElementHeader(bytes, index);
+            DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+            index += deHeaderArray.length;
+            @DataType int type = Objects.requireNonNull(deHeader).getDataType();
+            if (isSaltOrIdentity(type)) {
+                Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+                        + " and one identity in the advertisement.");
+                return new ArrayList<>();
+            }
+            byte[] deData = new byte[deHeader.getDataLength()];
+            for (int i = 0; i < deHeader.getDataLength(); i++) {
+                deData[i] = bytes[index++];
+            }
+            res.add(new DataElement(type, deData));
+        }
+        return res;
+    }
 
-        // Salt
-        DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
-        byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
-        mCompleteDataElementsBytes.add(saltByteArray);
-        length += saltByteArray.length;
+    @Nullable
+    private static byte[] getNonce(@DataType int type, byte[] data) {
+        try {
+            if (type == DataType.SALT) {
+                if (data.length != SALT_DATA_LENGTH) {
+                    Log.v(TAG, "Salt DataElement needs to be 2 bytes.");
+                    return null;
+                }
+                return CryptorMicImp.generateAdvNonce(data);
+            } else if (type == DataType.ENCRYPTION_INFO) {
+                try {
+                    EncryptionInfo info = new EncryptionInfo(data);
+                    if (info.getEncodingScheme() != EncryptionInfo.EncodingScheme.MIC) {
+                        Log.v(TAG, "Not support Signature yet.");
+                        return null;
+                    }
+                    return CryptorMicImp.generateAdvNonce(info.getSalt(), CIPHER_START_INDEX);
+                } catch (IllegalArgumentException e) {
+                    Log.w(TAG, "Salt DataElement needs to be 17 bytes.", e);
+                    return null;
+                }
+            }
+        }  catch (GeneralSecurityException e) {
+            Log.w(TAG, "Failed to decrypt metadata encryption key.", e);
+            return null;
+        }
+        return null;
+    }
 
-        // Identity
-        byte[] encryptedIdentity =
-                CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
-        DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
-        byte[] identityByteArray =
-                ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
-        mCompleteDataElementsBytes.add(identityByteArray);
-        length += identityByteArray.length;
-
-        List<Byte> dataElementBytes = new ArrayList<>();
-        // Intents
-        for (int action : mActions) {
-            DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
-                    new byte[] {(byte) action});
-            byte[] intentByteArray =
-                    ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
-            mCompleteDataElementsBytes.add(intentByteArray);
-            for (Byte b : intentByteArray) {
-                dataElementBytes.add(b);
+    @Nullable
+    private static List<Integer> getActionsFromDataElements(List<DataElement> dataElements) {
+        List<Integer> actions = new ArrayList<>();
+        for (DataElement dataElement : dataElements) {
+            if (dataElement.getKey() == DataElement.DataType.ACTION) {
+                byte[] value = dataElement.getValue();
+                if (value.length != 1) {
+                    Log.w(TAG, "Action should be only 1 byte.");
+                    return null;
+                }
+                actions.add(Byte.toUnsignedInt(value[0]));
             }
         }
-
-        // Data Elements (Extended properties)
-        for (DataElement dataElement : mDataElements) {
-            byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
-            mCompleteDataElementsBytes.add(deByteArray);
-            for (Byte b : deByteArray) {
-                dataElementBytes.add(b);
-            }
-        }
-
-        byte[] data = new byte[dataElementBytes.size()];
-        for (int i = 0; i < dataElementBytes.size(); i++) {
-            data[i] = dataElementBytes.get(i);
-        }
-        Cryptor cryptor = getCryptor(/* encrypt= */ true);
-        byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
-
-        length += encryptedDeBytes.length;
-
-        // Signature
-        byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
-        mHmacTag = hmacTag;
-        length += hmacTag.length;
-
-        this.mLength = length;
-    }
-
-    @PresenceCredential.IdentityType
-    private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
-        switch (type) {
-            case DataElement.DataType.PRIVATE_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_PRIVATE;
-            case DataElement.DataType.PROVISIONED_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
-            case DataElement.DataType.TRUSTED_IDENTITY:
-                return PresenceCredential.IDENTITY_TYPE_TRUSTED;
-            case DataElement.DataType.PUBLIC_IDENTITY:
-            default:
-                return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
-        }
-    }
-
-    @DataElement.DataType
-    private static int toDataType(@PresenceCredential.IdentityType int identityType) {
-        switch (identityType) {
-            case PresenceCredential.IDENTITY_TYPE_PRIVATE:
-                return DataElement.DataType.PRIVATE_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
-                return DataElement.DataType.PROVISIONED_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_TRUSTED:
-                return DataElement.DataType.TRUSTED_IDENTITY;
-            case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
-            default:
-                return DataElement.DataType.PUBLIC_IDENTITY;
-        }
-    }
-
-    /**
-     * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
-     * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
-     */
-    private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
-        return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
-                || type == DataElement.DataType.TRUSTED_IDENTITY
-                || type == DataElement.DataType.PROVISIONED_IDENTITY
-                || type == DataElement.DataType.PUBLIC_IDENTITY;
-    }
-
-    private static Cryptor getCryptor(boolean encrypt) {
-        if (encrypt) {
-            Log.d(TAG, "get V1 Cryptor");
-            return CryptorImpV1.getInstance();
-        }
-        Log.d(TAG, "get fake Cryptor");
-        return CryptorImpFake.getInstance();
+        return actions;
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
index 54264f7..50dada2 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceConstants.java
@@ -18,10 +18,15 @@
 
 import android.os.ParcelUuid;
 
+import com.android.server.nearby.util.ArrayUtils;
+
 /**
  * Constants for Nearby Presence operations.
  */
 public class PresenceConstants {
+    /** The Presence UUID value in byte array format. */
+    public static final byte[] PRESENCE_UUID_BYTES = ArrayUtils.intToByteArray(0xFCF1);
+
     /** Presence advertisement service data uuid. */
     public static final ParcelUuid PRESENCE_UUID =
             ParcelUuid.fromString("0000fcf1-0000-1000-8000-00805f9b34fb");
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 72edad0..66ae79c 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -85,6 +85,7 @@
                         case BroadcastRequest.PRESENCE_VERSION_V0:
                             bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
                                     advertiseData, this);
+                            Log.v(TAG, "Start to broadcast V0 advertisement.");
                             break;
                         case BroadcastRequest.PRESENCE_VERSION_V1:
                             if (adapter.isLeExtendedAdvertisingSupported()) {
@@ -92,6 +93,7 @@
                                         getAdvertisingSetParameters(),
                                         advertiseData,
                                         null, null, null, mAdvertisingSetCallback);
+                                Log.v(TAG, "Start to broadcast V1 advertisement.");
                             } else {
                                 Log.w(TAG, "Failed to start advertising set because the chipset"
                                         + " does not supports LE Extended Advertising feature.");
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index 355f7cf..e4651b7 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -45,7 +45,6 @@
 import com.android.server.nearby.presence.ExtendedAdvertisement;
 import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.ForegroundThread;
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
 
 import com.google.common.annotations.VisibleForTesting;
 
@@ -69,15 +68,6 @@
     @GuardedBy("mLock")
     @Nullable
     private List<android.nearby.ScanFilter> mScanFilters;
-    private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
-            new android.bluetooth.le.ScanCallback() {
-                @Override
-                public void onScanResult(int callbackType, ScanResult scanResult) {
-                }
-                @Override
-                public void onScanFailed(int errorCode) {
-                }
-            };
     private android.bluetooth.le.ScanCallback mScanCallback =
             new android.bluetooth.le.ScanCallback() {
                 @Override
@@ -133,10 +123,6 @@
                         .addMedium(NearbyDevice.Medium.BLE)
                         .setName(deviceName)
                         .setRssi(rssi);
-        for (int i : advertisement.getActions()) {
-            builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
-                    new byte[]{(byte) i}));
-        }
         for (DataElement dataElement : advertisement.getDataElements()) {
             builder.addExtendedProperty(dataElement);
         }
@@ -175,7 +161,6 @@
         if (isBleAvailable()) {
             Log.d(TAG, "BleDiscoveryProvider started");
             startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
-            startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
             return;
         }
         Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -192,7 +177,6 @@
         }
         Log.v(TAG, "Ble scan stopped.");
         bluetoothLeScanner.stopScan(mScanCallback);
-        bluetoothLeScanner.stopScan(mScanCallbackLegacy);
         synchronized (mLock) {
             if (mScanFilters != null) {
                 mScanFilters = null;
@@ -284,17 +268,13 @@
                         if (advertisement == null) {
                             continue;
                         }
-                        if (CryptorImpIdentityV1.getInstance().verify(
-                                advertisement.getIdentity(),
-                                credential.getEncryptedMetadataKeyTag())) {
-                            builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
-                                    rssi));
-                            builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
-                            if (!ArrayUtils.isEmpty(credential.getSecretId())) {
-                                builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
-                            }
-                            return;
+                        builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+                                rssi));
+                        builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+                        if (!ArrayUtils.isEmpty(credential.getSecretId())) {
+                            builder.setDeviceId(Arrays.hashCode(credential.getSecretId()));
                         }
+                        return;
                     }
                 }
             }
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index d69d42d..21ec252 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,6 +20,9 @@
 
 import static com.android.server.nearby.NearbyService.TAG;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
 import android.annotation.Nullable;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -155,7 +158,7 @@
             builder.setFastPairSupported(version != ChreCommunication.INVALID_NANO_APP_VERSION);
             try {
                 callback.onQueryComplete(builder.build());
-            } catch (RemoteException e) {
+            } catch (RemoteException | NullPointerException e) {
                 e.printStackTrace();
             }
         });
@@ -350,7 +353,11 @@
                                             DataElement.DataType.ACTION,
                                             new byte[]{(byte) filterResult.getIntent()}));
                         }
-
+                        if (filterResult.hasTimestampNs()) {
+                            presenceDeviceBuilder
+                                    .setDiscoveryTimestampMillis(MILLISECONDS.convert(
+                                            filterResult.getTimestampNs(), NANOSECONDS));
+                        }
                         PublicCredential publicCredential =
                                 new PublicCredential.Builder(
                                         secretId,
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
index bb5461c..e69d004 100644
--- a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -22,6 +22,10 @@
  * ArrayUtils class that help manipulate array.
  */
 public class ArrayUtils {
+    private static final char[] HEX_UPPERCASE = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+    };
+
     /** Concatenate N arrays of bytes into a single array. */
     public static byte[] concatByteArrays(byte[]... arrays) {
         // Degenerate case - no input provided.
@@ -65,4 +69,54 @@
         System.arraycopy(b, 0, result, 1, length);
         return result;
     }
+
+    /**
+     * Converts an Integer to a 2-byte array.
+     */
+    public static byte[] intToByteArray(int value) {
+        return new byte[] {(byte) (value >> 8), (byte) value};
+    }
+
+    /** Appends a byte to a byte array. */
+    public static byte[] append(byte[] a, byte b) {
+        if (a == null) {
+            return new byte[]{b};
+        }
+
+        int length = a.length;
+        byte[] result = new byte[length + 1];
+        System.arraycopy(a, 0, result, 0, length);
+        result[length] = b;
+        return result;
+    }
+
+    /** Convert an hex string to a byte array. */
+
+    public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+        int length = hex.length();
+        if (length % 2 != 0) {
+            throw new IllegalArgumentException("Hex string has odd number of characters");
+        }
+        byte[] out = new byte[length / 2];
+        for (int i = 0; i < length; i += 2) {
+            // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+            // our hex values are in 0, 255.
+            out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+        }
+        return out;
+    }
+
+    /** Encodes a byte array as a hexadecimal representation of bytes. */
+    public static String bytesToStringUppercase(byte[] bytes) {
+        int length = bytes.length;
+        StringBuilder out = new StringBuilder(length * 2);
+        for (int i = 0; i < length; i++) {
+            if (i == length - 1 && (bytes[i] & 0xff) == 0) {
+                break;
+            }
+            out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+            out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+        }
+        return out.toString();
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
index 3c5132d..ba9ca41 100644
--- a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -22,6 +22,10 @@
 
 import androidx.annotation.Nullable;
 
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 
@@ -30,22 +34,28 @@
 
 /** Class for encryption/decryption functionality. */
 public abstract class Cryptor {
+    /**
+     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+     */
+    public static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+    public static final byte[] NP_HKDF_SALT = "Google Nearby".getBytes(StandardCharsets.US_ASCII);
 
     /** AES only supports key sizes of 16, 24 or 32 bytes. */
     static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
 
-    private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+    public static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
 
     /**
      * Encrypt the provided data blob.
      *
      * @param data data blob to be encrypted.
-     * @param salt used for IV
+     * @param iv advertisement nonce
      * @param secretKeyBytes secrete key accessed from credentials
      * @return encrypted data, {@code null} if failed to encrypt.
      */
     @Nullable
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+    public byte[] encrypt(byte[] data, byte[] iv, byte[] secretKeyBytes) {
         return data;
     }
 
@@ -53,12 +63,12 @@
      * Decrypt the original data blob from the provided byte array.
      *
      * @param encryptedData data blob to be decrypted.
-     * @param salt used for IV
+     * @param iv advertisement nonce
      * @param secretKeyBytes secrete key accessed from credentials
      * @return decrypted data, {@code null} if failed to decrypt.
      */
     @Nullable
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+    public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] secretKeyBytes) {
         return encryptedData;
     }
 
@@ -90,7 +100,7 @@
      * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
      * given size.
      */
-    // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+    // Based on crypto/tink/subtle/Hkdf.java
     @Nullable
     static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
         Mac mac;
@@ -146,4 +156,66 @@
 
         return result;
     }
+
+    /**
+     * Computes an HKDF.
+     *
+     * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or
+     *                     "HMACSHA256".
+     * @param ikm          the input keying material.
+     * @param salt         optional salt. A possibly non-secret random value.
+     *                     (If no salt is provided i.e. if salt has length 0)
+     *                     then an array of 0s of the same size as the hash
+     *                     digest is used as salt.
+     * @param info         optional context and application specific information.
+     * @param size         The length of the generated pseudorandom string in bytes.
+     * @throws GeneralSecurityException if the {@code macAlgorithm} is not supported or if {@code
+     *                                  size} is too large or if {@code salt} is not a valid key for
+     *                                  macAlgorithm (which should not
+     *                                  happen since HMAC allows key sizes up to 2^64).
+     */
+    public static byte[] computeHkdf(
+            String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size)
+            throws GeneralSecurityException {
+        Mac mac = Mac.getInstance(macAlgorithm);
+        if (size > 255 * mac.getMacLength()) {
+            throw new GeneralSecurityException("size too large");
+        }
+        if (salt == null || salt.length == 0) {
+            // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
+            // then HKDF uses a salt that is an array of zeros of the same length as the hash
+            // digest.
+            mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
+        } else {
+            mac.init(new SecretKeySpec(salt, macAlgorithm));
+        }
+        byte[] prk = mac.doFinal(ikm);
+        byte[] result = new byte[size];
+        int ctr = 1;
+        int pos = 0;
+        mac.init(new SecretKeySpec(prk, macAlgorithm));
+        byte[] digest = new byte[0];
+        while (true) {
+            mac.update(digest);
+            mac.update(info);
+            mac.update((byte) ctr);
+            digest = mac.doFinal();
+            if (pos + digest.length < size) {
+                System.arraycopy(digest, 0, result, pos, digest.length);
+                pos += digest.length;
+                ctr++;
+            } else {
+                System.arraycopy(digest, 0, result, pos, size - pos);
+                break;
+            }
+        }
+        return result;
+    }
+
+    /** Generates the HMAC bytes. */
+    public static byte[] generateHmac(String algorithm, byte[] input, byte[] key) {
+        return Hashing.hmacSha256(new SecretKeySpec(key, algorithm))
+                .hashBytes(input)
+                .asBytes();
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
deleted file mode 100644
index 1c0ec9e..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import androidx.annotation.Nullable;
-
-/**
- * A Cryptor that returns the original data without actual encryption
- */
-public class CryptorImpFake extends Cryptor {
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable
-    private static CryptorImpFake sCryptor;
-
-    /** Returns an instance of CryptorImpFake. */
-    public static CryptorImpFake getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpFake();
-        }
-        return sCryptor;
-    }
-
-    private CryptorImpFake() {
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
deleted file mode 100644
index b0e19b4..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
+++ /dev/null
@@ -1,208 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
- * encryption and decryption.
- */
-public class CryptorImpIdentityV1 extends Cryptor {
-
-    // 3 16 byte arrays known by both the encryptor and decryptor.
-    private static final byte[] EK_IV =
-            new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
-    private static final byte[] ESALT_IV =
-            new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
-    private static final byte[] KTAG_IV =
-            {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
-
-    /** Length of encryption key required by AES/GCM encryption. */
-    private static final int ENCRYPTION_KEY_SIZE = 32;
-
-    /** Length of salt required by AES/GCM encryption. */
-    private static final int AES_CTR_IV_SIZE = 16;
-
-    /** Length HMAC tag */
-    public static final int HMAC_TAG_SIZE = 8;
-
-    /**
-     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
-     */
-    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
-    @VisibleForTesting
-    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable private static CryptorImpIdentityV1 sCryptor;
-
-    /** Returns an instance of CryptorImpIdentityV1. */
-    public static CryptorImpIdentityV1 getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpIdentityV1();
-        }
-        return sCryptor;
-    }
-
-    @Nullable
-    @Override
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Encrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
-        if (esalt == null) {
-            Log.e(TAG, "Failed to generate salt.");
-            return null;
-        }
-        try {
-            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-        try {
-            return cipher.doFinal(data);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-    }
-
-    @Nullable
-    @Override
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Decrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to get cipher instance.", e);
-            return null;
-        }
-        byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
-        if (esalt == null) {
-            return null;
-        }
-        try {
-            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-
-        try {
-            return cipher.doFinal(encryptedData);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
-            return null;
-        }
-    }
-
-    /**
-     * Generates a digital signature for the data.
-     *
-     * @return signature {@code null} if failed to sign
-     */
-    @Nullable
-    @Override
-    public byte[] sign(byte[] data, byte[] salt) {
-        if (data == null) {
-            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
-            return null;
-        }
-
-        // Generates a 8 bytes HMAC tag
-        return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
-    }
-
-    /**
-     * Generates a digital signature for the data.
-     * Uses KTAG_IV as salt value.
-     */
-    @Nullable
-    public byte[] sign(byte[] data) {
-        // Generates a 8 bytes HMAC tag
-        return sign(data, KTAG_IV);
-    }
-
-    @Override
-    public boolean verify(byte[] data, byte[] key, byte[] signature) {
-        return Arrays.equals(sign(data, key), signature);
-    }
-
-    /**
-     * Verifies the signature generated by data and key, with the original signed data. Uses
-     * KTAG_IV as salt value.
-     */
-    public boolean verify(byte[] data, byte[] signature) {
-        return verify(data, KTAG_IV, signature);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
deleted file mode 100644
index 15073fb..0000000
--- a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.android.server.nearby.NearbyService.TAG;
-
-import android.security.keystore.KeyProperties;
-import android.util.Log;
-
-import androidx.annotation.Nullable;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.security.NoSuchAlgorithmException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.NoSuchPaddingException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
- */
-public class CryptorImpV1 extends Cryptor {
-
-    /**
-     * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
-     */
-    private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
-
-    @VisibleForTesting
-    static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
-
-    /** Length of encryption key required by AES/GCM encryption. */
-    private static final int ENCRYPTION_KEY_SIZE = 32;
-
-    /** Length of salt required by AES/GCM encryption. */
-    private static final int AES_CTR_IV_SIZE = 16;
-
-    /** Length HMAC tag */
-    public static final int HMAC_TAG_SIZE = 16;
-
-    // 3 16 byte arrays known by both the encryptor and decryptor.
-    private static final byte[] AK_IV =
-            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-    private static final byte[] ASALT_IV =
-            new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
-    private static final byte[] HK_IV =
-            new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
-
-    // Lazily instantiated when {@link #getInstance()} is called.
-    @Nullable private static CryptorImpV1 sCryptor;
-
-    /** Returns an instance of CryptorImpV1. */
-    public static CryptorImpV1 getInstance() {
-        if (sCryptor == null) {
-            sCryptor = new CryptorImpV1();
-        }
-        return sCryptor;
-    }
-
-    private CryptorImpV1() {
-    }
-
-    @Nullable
-    @Override
-    public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Encrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
-        if (asalt == null) {
-            Log.e(TAG, "Failed to generate salt.");
-            return null;
-        }
-        try {
-            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-        try {
-            return cipher.doFinal(data);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to encrypt with secret key.", e);
-            return null;
-        }
-    }
-
-    @Nullable
-    @Override
-    public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.w(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes encryption key from authenticity_key
-        byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
-        if (encryptionKey == null) {
-            Log.e(TAG, "Failed to generate encryption key.");
-            return null;
-        }
-
-        // Decrypts the data using the encryption key
-        SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
-        Cipher cipher;
-        try {
-            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
-        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
-            Log.e(TAG, "Failed to get cipher instance.", e);
-            return null;
-        }
-        byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
-        if (asalt == null) {
-            return null;
-        }
-        try {
-            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
-        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
-            Log.e(TAG, "Failed to initialize cipher.", e);
-            return null;
-        }
-
-        try {
-            return cipher.doFinal(encryptedData);
-        } catch (IllegalBlockSizeException | BadPaddingException e) {
-            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
-            return null;
-        }
-    }
-
-    @Override
-    @Nullable
-    public byte[] sign(byte[] data, byte[] key) {
-        return generateHmacTag(data, key);
-    }
-
-    @Override
-    public int getSignatureLength() {
-        return HMAC_TAG_SIZE;
-    }
-
-    @Override
-    public boolean verify(byte[] data, byte[] key, byte[] signature) {
-        return Arrays.equals(sign(data, key), signature);
-    }
-
-    /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
-     * is equal to HMAC tag in advertisement to see data integrity. */
-    @Nullable
-    @VisibleForTesting
-    byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
-        if (data == null || authenticityKey == null) {
-            Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
-            return null;
-        }
-
-        if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
-            Log.e(TAG, "Illegal authenticity key size");
-            return null;
-        }
-
-        // Generates a 32 bytes HMAC key from authenticity_key
-        byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
-        if (hmacKey == null) {
-            Log.e(TAG, "Failed to generate HMAC key.");
-            return null;
-        }
-
-        // Generates a 16 bytes HMAC tag from authenticity_key
-        return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
-    }
-}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
new file mode 100644
index 0000000..3dbf85c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorMicImp.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2022 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.server.nearby.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.util.ArrayUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * MIC encryption and decryption for {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1}
+ * advertisement
+ */
+public class CryptorMicImp extends Cryptor {
+
+    public static final int MIC_LENGTH = 16;
+
+    private static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+    private static final byte[] AES_KEY_INFO_BYTES = "Unsigned Section AES key".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final byte[] ADV_NONCE_INFO_BYTES_SALT_DE = "Unsigned Section IV".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final byte[] ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE =
+            "V1 derived salt".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] METADATA_KEY_HMAC_KEY_INFO_BYTES =
+            "Unsigned Section metadata key HMAC key".getBytes(StandardCharsets.US_ASCII);
+    private static final byte[] MIC_HMAC_KEY_INFO_BYTES = "Unsigned Section HMAC key".getBytes(
+            StandardCharsets.US_ASCII);
+    private static final int AES_KEY_SIZE = 16;
+    private static final int ADV_NONCE_SIZE_SALT_DE = 16;
+    private static final int ADV_NONCE_SIZE_ENCRYPTION_INFO_DE = 12;
+    private static final int HMAC_KEY_SIZE = 32;
+
+    // Lazily instantiated when {@link #getInstance()} is called.
+    @Nullable
+    private static CryptorMicImp sCryptor;
+
+    private CryptorMicImp() {
+    }
+
+    /** Returns an instance of CryptorImpV1. */
+    public static CryptorMicImp getInstance() {
+        if (sCryptor == null) {
+            sCryptor = new CryptorMicImp();
+        }
+        return sCryptor;
+    }
+
+    /**
+     * Generate the meta data encryption key tag
+     * @param metadataEncryptionKey used as identity
+     * @param keySeed authenticity key saved in local and shared credential
+     * @return bytes generated by hmac or {@code null} when there is an error
+     */
+    @Nullable
+    public static byte[] generateMetadataEncryptionKeyTag(byte[] metadataEncryptionKey,
+            byte[] keySeed) {
+        try {
+            byte[] metadataKeyHmacKey = generateMetadataKeyHmacKey(keySeed);
+            return Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+                    metadataEncryptionKey, /* key= */ metadataKeyHmacKey);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Failed to generate Metadata encryption key tag.", e);
+            return null;
+        }
+    }
+
+    /**
+     * @param salt from the 2 bytes Salt Data Element
+     */
+    @Nullable
+    public static byte[] generateAdvNonce(byte[] salt) throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ salt,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ ADV_NONCE_INFO_BYTES_SALT_DE,
+                /* size= */ ADV_NONCE_SIZE_SALT_DE);
+    }
+
+    /** Generates the 12 bytes nonce with salt from the 2 bytes Salt Data Element */
+    @Nullable
+    public static byte[] generateAdvNonce(byte[] salt, int deIndex)
+            throws GeneralSecurityException {
+        // go/nearby-specs-working-doc
+        // Indices are encoded as big-endian unsigned 32-bit integers, starting at 1.
+        // Index 0 is reserved
+        byte[] indexBytes = new byte[4];
+        indexBytes[3] = (byte) deIndex;
+        byte[] info =
+                ArrayUtils.concatByteArrays(ADV_NONCE_INFO_BYTES_ENCRYPTION_INFO_DE, indexBytes);
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ salt,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ info,
+                /* size= */ ADV_NONCE_SIZE_ENCRYPTION_INFO_DE);
+    }
+
+    @Nullable
+    @Override
+    public byte[] encrypt(byte[] input, byte[] iv, byte[] keySeed) {
+        if (input == null || iv == null || keySeed == null) {
+            return null;
+        }
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+
+        byte[] aesKey;
+        try {
+            aesKey = generateAesKey(keySeed);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Encryption failed because failed to generate the AES key.", e);
+            return null;
+        }
+        if (aesKey == null) {
+            Log.i(TAG, "Failed to generate the AES key.");
+            return null;
+        }
+        SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+        try {
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+
+        }
+        try {
+            return cipher.doFinal(input);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to encrypt with secret key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    @Override
+    public byte[] decrypt(byte[] encryptedData, byte[] iv, byte[] keySeed) {
+        if (encryptedData == null || iv == null || keySeed == null) {
+            return null;
+        }
+
+        Cipher cipher;
+        try {
+            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            Log.e(TAG, "Failed to get cipher instance.", e);
+            return null;
+        }
+        byte[] aesKey;
+        try {
+            aesKey = generateAesKey(keySeed);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Decryption failed because failed to generate the AES key.", e);
+            return null;
+        }
+        SecretKey secretKey = new SecretKeySpec(aesKey, ENCRYPT_ALGORITHM);
+        try {
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+        } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Failed to initialize cipher.", e);
+            return null;
+        }
+
+        try {
+            return cipher.doFinal(encryptedData);
+        } catch (IllegalBlockSizeException | BadPaddingException e) {
+            Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+            return null;
+        }
+    }
+
+    @Override
+    @Nullable
+    public byte[] sign(byte[] data, byte[] key) {
+        byte[] res = generateHmacTag(data, key);
+        return res;
+    }
+
+    @Override
+    public int getSignatureLength() {
+        return MIC_LENGTH;
+    }
+
+    @Override
+    public boolean verify(byte[] data, byte[] key, byte[] signature) {
+        return Arrays.equals(sign(data, key), signature);
+    }
+
+    /**
+     * Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+     * is equal to HMAC tag in advertisement to see data integrity.
+     *
+     * @param input   concatenated advertisement UUID, header, section header, derived salt, and
+     *                section content
+     * @param keySeed the MIC HMAC key is calculated using the derived key
+     * @return the first 16 bytes of HMAC-SHA256 result
+     */
+    @Nullable
+    @VisibleForTesting
+    byte[] generateHmacTag(byte[] input, byte[] keySeed) {
+        try {
+            if (input == null || keySeed == null) {
+                return null;
+            }
+            byte[] micHmacKey = generateMicHmacKey(keySeed);
+            byte[] hmac = Cryptor.generateHmac(/* algorithm= */ HMAC_SHA256_ALGORITHM, /* input= */
+                    input, /* key= */ micHmacKey);
+            if (ArrayUtils.isEmpty(hmac)) {
+                return null;
+            }
+            return Arrays.copyOf(hmac, MIC_LENGTH);
+        } catch (GeneralSecurityException e) {
+            Log.e(TAG, "Failed to generate mic hmac key.", e);
+            return null;
+        }
+    }
+
+    @Nullable
+    private static byte[] generateAesKey(byte[] keySeed) throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ keySeed,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ AES_KEY_INFO_BYTES,
+                /* size= */ AES_KEY_SIZE);
+    }
+
+    private static byte[] generateMetadataKeyHmacKey(byte[] keySeed)
+            throws GeneralSecurityException {
+        return generateHmacKey(keySeed, METADATA_KEY_HMAC_KEY_INFO_BYTES);
+    }
+
+    private static byte[] generateMicHmacKey(byte[] keySeed) throws GeneralSecurityException {
+        return generateHmacKey(keySeed, MIC_HMAC_KEY_INFO_BYTES);
+    }
+
+    private static byte[] generateHmacKey(byte[] keySeed, byte[] info)
+            throws GeneralSecurityException {
+        return Cryptor.computeHkdf(
+                /* macAlgorithm= */ HMAC_SHA256_ALGORITHM,
+                /* ikm = */ keySeed,
+                /* salt= */ NP_HKDF_SALT,
+                /* info= */ info,
+                /* size= */ HMAC_KEY_SIZE);
+    }
+}
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index e1bf455..bf9357b 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -115,6 +115,9 @@
   repeated DataElement data_element = 7;
   optional bytes ble_service_data = 8;
   optional ResultType result_type = 9;
+  // Timestamp when the device is discovered, in nanoseconds,
+  // relative to Android SystemClock.elapsedRealtimeNanos().
+  optional uint64 timestamp_ns = 10;
 }
 
 message BleFilterResults {
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 66a1ffe..4309d7e 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -39,6 +39,7 @@
         "cts",
         "general-tests",
         "mts-tethering",
+        "mcts-tethering",
     ],
     certificate: "platform",
     sdk_version: "module_current",
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
index aa0dad3..0ca571a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerLegacyTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.managers;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
@@ -23,10 +24,12 @@
 
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -36,6 +39,8 @@
 import android.nearby.ScanRequest;
 import android.os.IBinder;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.provider.BleDiscoveryProvider;
 import com.android.server.nearby.provider.ChreCommunication;
@@ -86,6 +91,8 @@
     DiscoveryProviderManagerLegacy.ScanListenerDeathRecipient mScanListenerDeathRecipient;
     @Mock
     IBinder mIBinder;
+    @Mock
+    BluetoothAdapter mBluetoothAdapter;
     private DiscoveryProviderManagerLegacy mDiscoveryProviderManager;
     private Map<IBinder, DiscoveryProviderManagerLegacy.ScanListenerRecord>
             mScanTypeScanListenerRecordMap;
@@ -135,10 +142,15 @@
 
     @Before
     public void setup() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG);
         MockitoAnnotations.initMocks(this);
         when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
         when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
         when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
 
         mScanTypeScanListenerRecordMap = new HashMap<>();
         mDiscoveryProviderManager =
@@ -164,6 +176,13 @@
     }
 
     @Test
+    public void test_enableBleWhenBleOff() throws Exception {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        mDiscoveryProviderManager.init();
+        verify(mBluetoothAdapter, times(1)).enableBLE();
+    }
+
+    @Test
     public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
         when(mChreDiscoveryProvider.available()).thenReturn(true);
 
@@ -375,4 +394,62 @@
                 .isTrue();
         assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
     }
+
+    @Test
+    public void isBluetoothEnabledTest_bluetoothEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void isBluetoothEnabledTest_bleEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enableFailed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
+
+    @Test
+    public void enabledTest_scanIsOn() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_failed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
index 7ecf631..7cea34a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/managers/DiscoveryProviderManagerTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.managers;
 
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
 
@@ -24,10 +25,12 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AppOpsManager;
+import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.nearby.DataElement;
 import android.nearby.IScanListener;
@@ -37,6 +40,8 @@
 import android.nearby.ScanRequest;
 import android.os.IBinder;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.provider.BleDiscoveryProvider;
 import com.android.server.nearby.provider.ChreCommunication;
@@ -80,6 +85,8 @@
     CallerIdentity mCallerIdentity;
     @Mock
     IBinder mIBinder;
+    @Mock
+    BluetoothAdapter mBluetoothAdapter;
     private Executor mExecutor;
     private DiscoveryProviderManager mDiscoveryProviderManager;
 
@@ -128,12 +135,17 @@
 
     @Before
     public void setup() {
+        InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
+                READ_DEVICE_CONFIG);
         MockitoAnnotations.initMocks(this);
         mExecutor = Executors.newSingleThreadExecutor();
         when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
         when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
         when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
         when(mScanListener.asBinder()).thenReturn(mIBinder);
+        when(mInjector.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
 
         mDiscoveryProviderManager =
                 new DiscoveryProviderManager(mContext, mExecutor, mInjector,
@@ -157,6 +169,13 @@
     }
 
     @Test
+    public void test_enableBleWhenBleOff() throws Exception {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        mDiscoveryProviderManager.init();
+        verify(mBluetoothAdapter, times(1)).enableBLE();
+    }
+
+    @Test
     public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
         reset(mBluetoothController);
         when(mChreDiscoveryProvider.available()).thenReturn(true);
@@ -336,4 +355,62 @@
                 .isTrue();
         assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
     }
+
+    @Test
+    public void isBluetoothEnabledTest_bluetoothEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void isBluetoothEnabledTest_bleEnabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enabled() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(true);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_enableFailed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(true);
+        when(mBluetoothAdapter.enableBLE()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
+
+    @Test
+    public void enabledTest_scanIsOn() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isTrue();
+    }
+
+    @Test
+    public void enabledTest_failed() {
+        when(mBluetoothAdapter.isEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isLeEnabled()).thenReturn(false);
+        when(mBluetoothAdapter.isBleScanAlwaysAvailable()).thenReturn(false);
+
+        assertThat(mDiscoveryProviderManager.setBleScanEnabled()).isFalse();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
index a1eb57b..6ec7c57 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/EncryptionInfoTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertThrows;
 
+import com.android.server.nearby.presence.EncryptionInfo.EncodingScheme;
 import com.android.server.nearby.util.ArrayUtils;
 
 import org.junit.Test;
@@ -50,7 +51,7 @@
     public void test_getMethods_signature() {
         byte[] data = ArrayUtils.append((byte) 0b10001000, SALT);
         EncryptionInfo info = new EncryptionInfo(data);
-        assertThat(info.getEncodingScheme()).isEqualTo(EncryptionInfo.EncodingScheme.SIGNATURE);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.SIGNATURE);
         assertThat(info.getSalt()).isEqualTo(SALT);
     }
 
@@ -58,7 +59,14 @@
     public void test_getMethods_mic() {
         byte[] data = ArrayUtils.append((byte) 0b10000000, SALT);
         EncryptionInfo info = new EncryptionInfo(data);
-        assertThat(info.getEncodingScheme()).isEqualTo(EncryptionInfo.EncodingScheme.MIC);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
+        assertThat(info.getSalt()).isEqualTo(SALT);
+    }
+    @Test
+    public void test_toBytes() {
+        byte[] data = EncryptionInfo.toByte(EncodingScheme.MIC, SALT);
+        EncryptionInfo info = new EncryptionInfo(data);
+        assertThat(info.getEncodingScheme()).isEqualTo(EncodingScheme.MIC);
         assertThat(info.getSalt()).isEqualTo(SALT);
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
index 895df69..3f00a42 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.presence;
 
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID_BYTES;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.nearby.BroadcastRequest;
@@ -25,19 +27,22 @@
 import android.nearby.PrivateCredential;
 import android.nearby.PublicCredential;
 
-import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
-import com.android.server.nearby.util.encryption.CryptorImpV1;
+import com.android.server.nearby.util.ArrayUtils;
+import com.android.server.nearby.util.encryption.CryptorMicImp;
 
 import org.junit.Before;
 import org.junit.Test;
 
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
 public class ExtendedAdvertisementTest {
+    private static final int EXTENDED_ADVERTISEMENT_BYTE_LENGTH = 67;
     private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+    private static final int DATA_TYPE_ACTION = 6;
     private static final int DATA_TYPE_MODEL_ID = 7;
     private static final int DATA_TYPE_BLE_ADDRESS = 101;
     private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
@@ -49,18 +54,23 @@
     private static final DataElement BLE_ADDRESS_ELEMENT =
             new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
 
-    private static final byte[] IDENTITY =
-            new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+    private static final byte[] METADATA_ENCRYPTION_KEY =
+            new byte[]{-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
     private static final int MEDIUM_TYPE_BLE = 0;
     private static final byte[] SALT = {2, 3};
+
     private static final int PRESENCE_ACTION_1 = 1;
     private static final int PRESENCE_ACTION_2 = 2;
+    private static final DataElement PRESENCE_ACTION_DE_1 =
+            new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_1});
+    private static final DataElement PRESENCE_ACTION_DE_2 =
+            new DataElement(DATA_TYPE_ACTION, new byte[]{PRESENCE_ACTION_2});
 
     private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
     private static final byte[] AUTHENTICITY_KEY =
             new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
     private static final byte[] PUBLIC_KEY =
-            new byte[] {
+            new byte[]{
                     48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
                     66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
                     -83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
@@ -68,7 +78,7 @@
                     123, 41, -119, -25, 1, -112, 112
             };
     private static final byte[] ENCRYPTED_METADATA_BYTES =
-            new byte[] {
+            new byte[]{
                     -44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
                     -18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
                     88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
@@ -78,23 +88,65 @@
                     -4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
             };
     private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
-            new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+            new byte[]{-100, 102, -35, -99, 66, -85, -55, -58, -52, 11, -74, 102, 109, -89, 1, -34,
+                    45, 43, 107, -60, 99, -21, 28, 34, 31, -100, -96, 108, 108, -18, 107, 5};
+
+    private static final String ENCODED_ADVERTISEMENT_ENCRYPTION_INFO =
+            "2091911000DE2A89ED98474AF3E41E48487E8AEBDE90014C18BCB9F9AAC5C11A1BE00A10A5DCD2C49A74BE"
+                    + "BAF0FE72FD5053B9DF8B9976C80BE0DCE8FEE83F1BFA9A89EB176CA48EE4ED5D15C6CDAD6B9E"
+                    + "41187AA6316D7BFD8E454A53971AC00836F7AB0771FF0534050037D49C6AEB18CF9F8590E5CD"
+                    + "EE2FBC330FCDC640C63F0735B7E3F02FE61A0496EF976A158AD3455D";
+    private static final byte[] METADATA_ENCRYPTION_KEY_TAG_2 =
+            new byte[]{-54, -39, 41, 16, 61, 79, -116, 14, 94, 0, 84, 45, 26, -108, 66, -48, 124,
+                    -81, 61, 56, -98, -47, 14, -19, 116, 106, -27, 123, -81, 49, 83, -42};
+
     private static final String DEVICE_NAME = "test_device";
 
+    private static final byte[] SALT_16 =
+            ArrayUtils.stringToBytes("DE2A89ED98474AF3E41E48487E8AEBDE");
+    private static final byte[] AUTHENTICITY_KEY_2 =  ArrayUtils.stringToBytes(
+            "959D2F3CAB8EE4A2DEB0255C03762CF5D39EB919300420E75A089050FB025E20");
+    private static final byte[] METADATA_ENCRYPTION_KEY_2 =  ArrayUtils.stringToBytes(
+            "EF5E9A0867560E52AE1F05FCA7E48D29");
+
+    private static final DataElement DE1 = new DataElement(571, ArrayUtils.stringToBytes(
+            "537F96FD94E13BE589F0141145CFC0EEC4F86FBDB2"));
+    private static final DataElement DE2 = new DataElement(541, ArrayUtils.stringToBytes(
+            "D301FFB24B5B"));
+    private static final DataElement DE3 = new DataElement(51, ArrayUtils.stringToBytes(
+            "EA95F07C25B75C04E1B2B8731F6A55BA379FB141"));
+    private static final DataElement DE4 = new DataElement(729, ArrayUtils.stringToBytes(
+            "2EFD3101E2311BBB108F0A7503907EAF0C2EAAA60CDA8D33A294C4CEACE0"));
+    private static final DataElement DE5 = new DataElement(411, ArrayUtils.stringToBytes("B0"));
+
     private PresenceBroadcastRequest.Builder mBuilder;
+    private PresenceBroadcastRequest.Builder mBuilderCredentialInfo;
     private PrivateCredential mPrivateCredential;
+    private PrivateCredential mPrivateCredential2;
+
     private PublicCredential mPublicCredential;
+    private PublicCredential mPublicCredential2;
 
     @Before
     public void setUp() {
         mPrivateCredential =
-                new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+                new PrivateCredential.Builder(
+                        SECRET_ID, AUTHENTICITY_KEY, METADATA_ENCRYPTION_KEY, DEVICE_NAME)
+                        .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+                        .build();
+        mPrivateCredential2 =
+                new PrivateCredential.Builder(
+                        SECRET_ID, AUTHENTICITY_KEY_2, METADATA_ENCRYPTION_KEY_2, DEVICE_NAME)
                         .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
                         .build();
         mPublicCredential =
                 new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
                         ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
                         .build();
+        mPublicCredential2 =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY_2, PUBLIC_KEY,
+                        ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG_2)
+                        .build();
         mBuilder =
                 new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
                         SALT, mPrivateCredential)
@@ -103,6 +155,16 @@
                         .addAction(PRESENCE_ACTION_2)
                         .addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
                         .addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+
+        mBuilderCredentialInfo =
+                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+                        SALT_16, mPrivateCredential2)
+                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+                        .addExtendedProperty(DE1)
+                        .addExtendedProperty(DE2)
+                        .addExtendedProperty(DE3)
+                        .addExtendedProperty(DE4)
+                        .addExtendedProperty(DE5);
     }
 
     @Test
@@ -112,36 +174,50 @@
 
         assertThat(originalAdvertisement.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(originalAdvertisement.getLength()).isEqualTo(66);
         assertThat(originalAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
         assertThat(originalAdvertisement.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2,
+                        MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+        assertThat(originalAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
+    }
+
+    @Test
+    public void test_createFromRequest_credentialInfo() {
+        ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+                mBuilderCredentialInfo.build());
+
+        assertThat(originalAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+        assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(originalAdvertisement.getVersion()).isEqualTo(
+                BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT_16);
+        assertThat(originalAdvertisement.getDataElements())
+                .containsExactly(DE1, DE2, DE3, DE4, DE5);
     }
 
     @Test
     public void test_createFromRequest_encodeAndDecode() {
         ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
                 mBuilder.build());
-
         byte[] generatedBytes = originalAdvertisement.toBytes();
-
         ExtendedAdvertisement newAdvertisement =
                 ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
 
         assertThat(newAdvertisement.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(newAdvertisement.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(newAdvertisement.getLength()).isEqualTo(66);
+        assertThat(newAdvertisement.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         assertThat(newAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
         assertThat(newAdvertisement.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+                        PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
     }
 
     @Test
@@ -170,45 +246,77 @@
                         .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
                         .addAction(PRESENCE_ACTION_1);
         assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
-
-        // empty action
-        PresenceBroadcastRequest.Builder builder3 =
-                new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
-                        SALT, mPrivateCredential)
-                        .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
-        assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
     }
 
     @Test
-    public void test_toBytes() {
+    public void test_toBytesSalt() throws Exception {
         ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
         assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
     }
 
     @Test
-    public void test_fromBytes() {
+    public void test_fromBytesSalt() throws Exception {
         byte[] originalBytes = getExtendedAdvertisementByteArray();
         ExtendedAdvertisement adv =
                 ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
 
         assertThat(adv.getActions())
                 .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
-        assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+        assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY);
         assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
-        assertThat(adv.getLength()).isEqualTo(66);
+        assertThat(adv.getLength()).isEqualTo(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         assertThat(adv.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V1);
         assertThat(adv.getSalt()).isEqualTo(SALT);
         assertThat(adv.getDataElements())
-                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+                .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT,
+                        PRESENCE_ACTION_DE_1, PRESENCE_ACTION_DE_2);
+    }
+
+    @Test
+    public void test_toBytesCredentialElement() {
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.createFromRequest(mBuilderCredentialInfo.build());
+        assertThat(ArrayUtils.bytesToStringUppercase(adv.toBytes())).isEqualTo(
+                ENCODED_ADVERTISEMENT_ENCRYPTION_INFO);
+    }
+
+    @Test
+    public void test_fromBytesCredentialElement() {
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.fromBytes(
+                        ArrayUtils.stringToBytes(ENCODED_ADVERTISEMENT_ENCRYPTION_INFO),
+                        mPublicCredential2);
+        assertThat(adv.getIdentity()).isEqualTo(METADATA_ENCRYPTION_KEY_2);
+        assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+        assertThat(adv.getVersion()).isEqualTo(BroadcastRequest.PRESENCE_VERSION_V1);
+        assertThat(adv.getSalt()).isEqualTo(SALT_16);
+        assertThat(adv.getDataElements()).containsExactly(DE1, DE2, DE3, DE4, DE5);
+    }
+
+    @Test
+    public void test_fromBytes_metadataTagNotMatched_fail() throws Exception {
+        byte[] originalBytes = getExtendedAdvertisementByteArray();
+        PublicCredential credential =
+                new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+                        ENCRYPTED_METADATA_BYTES,
+                        new byte[]{113, 90, -55, 73, 25, -9, 55, -44, 102, 44, 81, -68, 101, 21, 32,
+                                92, -107, 3, 108, 90, 28, -73, 16, 49, -95, -121, 8, -45, -27, 16,
+                                6, 108})
+                        .build();
+        ExtendedAdvertisement adv =
+                ExtendedAdvertisement.fromBytes(originalBytes, credential);
+        assertThat(adv).isNull();
     }
 
     @Test
     public void test_toString() {
         ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
         assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
-                + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
-                + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+                + "<VERSION: 1, length: " + EXTENDED_ADVERTISEMENT_BYTE_LENGTH
+                + ", dataElementCount: 4, identityType: 1, "
+                + "identity: " + Arrays.toString(METADATA_ENCRYPTION_KEY)
+                + ", salt: [2, 3],"
                 + " actions: [1, 2]>");
     }
 
@@ -222,26 +330,22 @@
         assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
     }
 
-    private static byte[] getExtendedAdvertisementByteArray() {
-        ByteBuffer buffer = ByteBuffer.allocate(66);
+    private static byte[] getExtendedAdvertisementByteArray() throws Exception {
+        CryptorMicImp cryptor = CryptorMicImp.getInstance();
+        ByteBuffer buffer = ByteBuffer.allocate(EXTENDED_ADVERTISEMENT_BYTE_LENGTH);
         buffer.put((byte) 0b00100000); // Header V1
-        buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+        buffer.put(
+                (byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)); // Section header (section length)
+
         // Salt data
-        buffer.put(SALT);
+        // Salt header: length 2, type 0
+        byte[] saltBytes = ArrayUtils.concatByteArrays(new byte[]{(byte) 0b00100000}, SALT);
+        buffer.put(saltBytes);
         // Identity header: length 16, type 1 (private identity)
-        buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
-        // Identity data
-        buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+        byte[] identityHeader = new byte[]{(byte) 0b10010000, (byte) 0b00000001};
+        buffer.put(identityHeader);
 
         ByteBuffer deBuffer = ByteBuffer.allocate(28);
-        // Action1 header: length 1, type 6
-        deBuffer.put(new byte[]{(byte) 0b00010110});
-        // Action1 data
-        deBuffer.put((byte) PRESENCE_ACTION_1);
-        // Action2 header: length 1, type 6
-        deBuffer.put(new byte[]{(byte) 0b00010110});
-        // Action2 data
-        deBuffer.put((byte) PRESENCE_ACTION_2);
         // Ble address header: length 7, type 102
         deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
         // Ble address data
@@ -250,11 +354,30 @@
         deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
         // model id data
         deBuffer.put(MODE_ID_DATA);
+        // Action1 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action1 data
+        deBuffer.put((byte) PRESENCE_ACTION_1);
+        // Action2 header: length 1, type 6
+        deBuffer.put(new byte[]{(byte) 0b00010110});
+        // Action2 data
+        deBuffer.put((byte) PRESENCE_ACTION_2);
+        byte[] deBytes = deBuffer.array();
+        byte[] nonce = CryptorMicImp.generateAdvNonce(SALT);
+        byte[] ciphertext =
+                cryptor.encrypt(
+                        ArrayUtils.concatByteArrays(METADATA_ENCRYPTION_KEY, deBytes),
+                        nonce, AUTHENTICITY_KEY);
+        buffer.put(ciphertext);
 
-        byte[] data = deBuffer.array();
-        CryptorImpV1 cryptor = CryptorImpV1.getInstance();
-        buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
-        buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+        byte[] dataToSign = ArrayUtils.concatByteArrays(
+                PRESENCE_UUID_BYTES, /* UUID */
+                new byte[]{(byte) 0b00100000}, /* header */
+                new byte[]{(byte) (EXTENDED_ADVERTISEMENT_BYTE_LENGTH - 2)} /* sectionHeader */,
+                saltBytes, /* salt */
+                nonce, identityHeader, ciphertext);
+        byte[] mic = cryptor.sign(dataToSign, AUTHENTICITY_KEY);
+        buffer.put(mic);
 
         return buffer.array();
     }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index 05b556b..0b1a742 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.provider;
 
+import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.times;
@@ -69,7 +70,7 @@
 
         AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
         mBleBroadcastProvider.onStartSuccess(settings);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
@@ -81,7 +82,7 @@
         // advertising set can not be mocked, so we will allow nulls
         mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
                 AdvertisingSetCallback.ADVERTISE_SUCCESS);
-        verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+        verify(mBroadcastListener, atLeast(1)).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 154441b..590a46e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -192,7 +192,12 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
-    public void testOnNearbyDeviceDiscoveredWithDataElements() {
+    public void testOnNearbyDeviceDiscoveredWithDataElements_TIME() {
+        // The feature only supports user-debug builds.
+        if (!Build.isDebuggable()) {
+            return;
+        }
+
         // Disables the setting of test app support
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
@@ -209,6 +214,7 @@
         // First byte is length of service data, padding zeros should be thrown away.
         final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
         final byte [] testData = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+        final long timestampNs = 1697765417070000000L;
 
         final List<DataElement> expectedExtendedProperties = new ArrayList<>();
         expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
@@ -262,6 +268,7 @@
                                 .setValue(ByteString.copyFrom(testData))
                                 .setValueLength(testData.length)
                         )
+                        .setTimestampNs(timestampNs)
                         .build();
         Blefilter.BleFilterResults results =
                 Blefilter.BleFilterResults.newBuilder().addResult(result).build();
@@ -285,11 +292,18 @@
             DeviceConfig.setProperty(NAMESPACE, NEARBY_SUPPORT_TEST_APP, "true", false);
             assertThat(new NearbyConfiguration().isTestAppSupported()).isTrue();
         }
+        // Nanoseconds to Milliseconds
+        assertThat((mNearbyDevice.getValue().getPresenceDevice())
+                .getDiscoveryTimestampMillis()).isEqualTo(timestampNs / 1000000);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testOnNearbyDeviceDiscoveredWithTestDataElements() {
+        // The feature only supports user-debug builds.
+        if (!Build.isDebuggable()) {
+            return;
+        }
         // Enables the setting of test app support
         boolean isSupportedTestApp = getDeviceConfigBoolean(
                 NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
index a759baf..455c432 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import androidx.test.filters.SdkSuppress;
-
 import org.junit.Test;
 
 public final class ArrayUtilsTest {
@@ -30,51 +28,58 @@
     private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysNoInput() {
         assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysOneEmptyArray() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysOneNonEmptyArray() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysMultipleNonEmptyArrays() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testConcatByteArraysMultipleArrays() {
         assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
                 .isEqualTo(BYTES_ALL);
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmptyNull_returnsTrue() {
         assertThat(ArrayUtils.isEmpty(null)).isTrue();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmpty_returnsTrue() {
         assertThat(ArrayUtils.isEmpty(new byte[]{})).isTrue();
     }
 
     @Test
-    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testIsEmpty_returnsFalse() {
         assertThat(ArrayUtils.isEmpty(BYTES_ALL)).isFalse();
     }
+
+    @Test
+    public void testAppendByte() {
+        assertThat(ArrayUtils.append((byte) 2, BYTES_ONE)).isEqualTo(new byte[]{2, 7, 9});
+    }
+
+    @Test
+    public void testAppendByteNull() {
+        assertThat(ArrayUtils.append((byte) 2, null)).isEqualTo(new byte[]{2});
+    }
+
+    @Test
+    public void testAppendByteToArray() {
+        assertThat(ArrayUtils.append(BYTES_ONE, (byte) 2)).isEqualTo(new byte[]{7, 9, 2});
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
deleted file mode 100644
index f0294fc..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-public class CryptorImpIdentityV1Test {
-    private static final String TAG = "CryptorImpIdentityV1Test";
-    private static final byte[] SALT = new byte[] {102, 22};
-    private static final byte[] DATA =
-            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
-    private static final byte[] AUTHENTICITY_KEY =
-            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
-    @Test
-    public void test_encrypt_decrypt() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
-    }
-
-    @Test
-    public void test_encryption() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        // for debugging
-        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
-        assertThat(encryptedData).isEqualTo(getEncryptedData());
-    }
-
-    @Test
-    public void test_decryption() {
-        Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] decryptedData =
-                identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
-        // for debugging
-        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
-        assertThat(decryptedData).isEqualTo(DATA);
-    }
-
-    @Test
-    public void generateHmacTag() {
-        CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
-        byte[] generatedTag = identityCryptor.sign(DATA);
-        byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
-        assertThat(generatedTag).isEqualTo(expectedTag);
-    }
-
-    private static byte[] getEncryptedData() {
-        return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
deleted file mode 100644
index 3ca2575..0000000
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2022 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.server.nearby.util.encryption;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.util.Log;
-
-import org.junit.Test;
-
-import java.util.Arrays;
-
-/**
- * Unit test for {@link CryptorImpV1}
- */
-public final class CryptorImpV1Test {
-    private static final String TAG = "CryptorImpV1Test";
-    private static final byte[] SALT = new byte[] {102, 22};
-    private static final byte[] DATA =
-            new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
-    private static final byte[] AUTHENTICITY_KEY =
-            new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
-
-    @Test
-    public void test_encryption() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
-
-        // for debugging
-        Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
-
-        assertThat(encryptedData).isEqualTo(getEncryptedData());
-    }
-
-    @Test
-    public void test_encryption_invalidInput() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
-    }
-
-    @Test
-    public void test_decryption() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        byte[] decryptedData =
-                v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
-        // for debugging
-        Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
-
-        assertThat(decryptedData).isEqualTo(DATA);
-    }
-
-    @Test
-    public void test_decryption_invalidInput() {
-        Cryptor v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
-    }
-
-    @Test
-    public void generateSign() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
-        byte[] expectedTag = new byte[]{
-                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-        assertThat(generatedTag).isEqualTo(expectedTag);
-    }
-
-    @Test
-    public void test_verify() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] expectedTag = new byte[]{
-                100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
-
-        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
-        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
-    }
-
-    @Test
-    public void test_generateHmacTag_sameResult() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
-        assertThat(res1)
-                .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
-    }
-
-    @Test
-    public void test_generateHmacTag_nullData() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
-    }
-
-    @Test
-    public void test_generateHmacTag_nullKey() {
-        CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
-        assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
-    }
-
-    private static byte[] getEncryptedData() {
-        return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
-    }
-}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
new file mode 100644
index 0000000..b6d2333
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorMicImpTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.server.nearby.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+/**
+ * Unit test for {@link CryptorMicImp}
+ */
+public final class CryptorMicImpTest {
+    private static final String TAG = "CryptorImpV1Test";
+    private static final byte[] SALT = new byte[]{102, 22};
+    private static final byte[] DATA =
+            new byte[]{107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+    private static final byte[] AUTHENTICITY_KEY =
+            new byte[]{-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+    private static byte[] getEncryptedData() {
+        return new byte[]{112, 23, -111, 87, 122, -27, 45, -25, -35, 84, -89, 115, 61, 113};
+    }
+
+    @Test
+    public void test_encryption() throws Exception {
+        Cryptor v1Cryptor = CryptorMicImp.getInstance();
+        byte[] encryptedData =
+                v1Cryptor.encrypt(DATA,  CryptorMicImp.generateAdvNonce(SALT), AUTHENTICITY_KEY);
+        assertThat(encryptedData).isEqualTo(getEncryptedData());
+    }
+
+    @Test
+    public void test_decryption() throws Exception {
+        Cryptor v1Cryptor = CryptorMicImp.getInstance();
+        byte[] decryptedData =
+                v1Cryptor.decrypt(getEncryptedData(), CryptorMicImp.generateAdvNonce(SALT),
+                        AUTHENTICITY_KEY);
+        assertThat(decryptedData).isEqualTo(DATA);
+    }
+
+    @Test
+    public void test_verify() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        byte[] expectedTag = new byte[]{
+                -80, -51, -101, -7, -65, 110, 37, 68, 122, -128, 57, -90, -115, -59, -61, 46};
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+        assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+    }
+
+    @Test
+    public void test_generateHmacTag_sameResult() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+        assertThat(res1)
+                .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+    }
+
+    @Test
+    public void test_generateHmacTag_nullData() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+    }
+
+    @Test
+    public void test_generateHmacTag_nullKey() {
+        CryptorMicImp v1Cryptor = CryptorMicImp.getInstance();
+        assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
index ca612e3..1fb2236 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -42,7 +42,7 @@
         assertThat(res2).hasLength(outputSize);
         assertThat(res1).isNotEqualTo(res2);
         assertThat(res1)
-                .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+                .isEqualTo(CryptorMicImp.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
     }
 
     @Test
diff --git a/service-t/src/com/android/server/IpSecService.java b/service-t/src/com/android/server/IpSecService.java
index a884840..ea91e64 100644
--- a/service-t/src/com/android/server/IpSecService.java
+++ b/service-t/src/com/android/server/IpSecService.java
@@ -42,6 +42,7 @@
 import android.net.IpSecSpiResponse;
 import android.net.IpSecTransform;
 import android.net.IpSecTransformResponse;
+import android.net.IpSecTransformState;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.net.IpSecUdpEncapResponse;
 import android.net.LinkAddress;
@@ -70,6 +71,7 @@
 import com.android.net.module.util.BinderUtils;
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
 
 import libcore.io.IoUtils;
 
@@ -109,6 +111,7 @@
     @VisibleForTesting static final int MAX_PORT_BIND_ATTEMPTS = 10;
 
     private final INetd mNetd;
+    private final IpSecXfrmController mIpSecXfrmCtrl;
 
     static {
         try {
@@ -152,6 +155,11 @@
             }
             return netd;
         }
+
+        /** Get a instance of IpSecXfrmController */
+        public IpSecXfrmController getIpSecXfrmController() {
+            return new IpSecXfrmController();
+        }
     }
 
     final UidFdTagger mUidFdTagger;
@@ -1111,6 +1119,7 @@
         mContext = context;
         mDeps = Objects.requireNonNull(deps, "Missing dependencies.");
         mUidFdTagger = uidFdTagger;
+        mIpSecXfrmCtrl = mDeps.getIpSecXfrmController();
         try {
             mNetd = mDeps.getNetdInstance(mContext);
         } catch (RemoteException e) {
@@ -1862,6 +1871,48 @@
         releaseResource(userRecord.mTransformRecords, resourceId);
     }
 
+    @Override
+    public synchronized IpSecTransformState getTransformState(int transformId)
+            throws IllegalStateException, RemoteException {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.ACCESS_NETWORK_STATE, "IpsecService#getTransformState");
+
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        TransformRecord transformInfo =
+                userRecord.mTransformRecords.getResourceOrThrow(transformId);
+
+        final int spi = transformInfo.getSpiRecord().getSpi();
+        final InetAddress destAddress =
+                InetAddresses.parseNumericAddress(
+                        transformInfo.getConfig().getDestinationAddress());
+        Log.d(TAG, "getTransformState for spi " + spi + " destAddress " + destAddress);
+
+        // Make netlink call
+        final XfrmNetlinkNewSaMessage xfrmNewSaMsg;
+        try {
+            xfrmNewSaMsg = mIpSecXfrmCtrl.ipSecGetSa(destAddress, Integer.toUnsignedLong(spi));
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "getTransformState: failed to get IpSecTransformState" + e.toString());
+            throw new IllegalStateException("Failed to get IpSecTransformState", e);
+        }
+
+        // Keep the netlink socket open to save time for the next call. It is cheap to have a
+        // persistent netlink socket in the system server
+
+        if (xfrmNewSaMsg == null) {
+            Log.e(TAG, "getTransformState: failed to get IpSecTransformState xfrmNewSaMsg is null");
+            throw new IllegalStateException("Failed to get IpSecTransformState");
+        }
+
+        return new IpSecTransformState.Builder()
+                .setTxHighestSequenceNumber(xfrmNewSaMsg.getTxSequenceNumber())
+                .setRxHighestSequenceNumber(xfrmNewSaMsg.getRxSequenceNumber())
+                .setPacketCount(xfrmNewSaMsg.getPacketCount())
+                .setByteCount(xfrmNewSaMsg.getByteCount())
+                .setReplayBitmap(xfrmNewSaMsg.getBitmap())
+                .build();
+    }
+
     /**
      * Apply an active transport mode transform to a socket, which will apply the IPsec security
      * association as a correspondent policy to the provided socket
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 2640332..9c01dda 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -111,7 +111,9 @@
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -170,6 +172,8 @@
             "mdns_advertiser_allowlist_";
     private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
 
+    private static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
     @VisibleForTesting
     static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
             "mdns_config_running_app_active_importance_cutoff";
@@ -186,6 +190,7 @@
     static final int NO_TRANSACTION = -1;
     private static final int NO_SENT_QUERY_COUNT = 0;
     private static final int DISCOVERY_QUERY_SENT_CALLBACK = 1000;
+    private static final int MAX_SUBTYPE_COUNT = 100;
     private static final SharedLog LOGGER = new SharedLog("serviceDiscovery");
 
     private final Context mContext;
@@ -688,6 +693,35 @@
                 return mClients.get(args.connector);
             }
 
+            /**
+             * Returns {@code false} if {@code subtypes} exceeds the maximum number limit or
+             * contains invalid subtype label.
+             */
+            private boolean checkSubtypeLabels(Set<String> subtypes) {
+                if (subtypes.size() > MAX_SUBTYPE_COUNT) {
+                    mServiceLogs.e(
+                            "Too many subtypes: " + subtypes.size() + " (max = "
+                                    + MAX_SUBTYPE_COUNT + ")");
+                    return false;
+                }
+
+                for (String subtype : subtypes) {
+                    if (!checkSubtypeLabel(subtype)) {
+                        mServiceLogs.e("Subtype " + subtype + " is invalid");
+                        return false;
+                    }
+                }
+                return true;
+            }
+
+            private Set<String> dedupSubtypeLabels(Collection<String> subtypes) {
+                final Map<String, String> subtypeMap = new LinkedHashMap<>(subtypes.size());
+                for (String subtype : subtypes) {
+                    subtypeMap.put(MdnsUtils.toDnsLowerCase(subtype), subtype);
+                }
+                return new ArraySet<>(subtypeMap.values());
+            }
+
             @Override
             public boolean processMessage(Message msg) {
                 final ClientInfo clientInfo;
@@ -846,13 +880,23 @@
                             serviceInfo.setServiceName(truncateServiceName(
                                     serviceInfo.getServiceName()));
 
+                            Set<String> subtypes = new ArraySet<>(serviceInfo.getSubtypes());
+                            if (!TextUtils.isEmpty(typeSubtype.second)) {
+                                subtypes.add(typeSubtype.second);
+                            }
+
+                            subtypes = dedupSubtypeLabels(subtypes);
+
+                            if (!checkSubtypeLabels(subtypes)) {
+                                clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+                                        NsdManager.FAILURE_BAD_PARAMETERS, false /* isLegacy */);
+                                break;
+                            }
+
+                            serviceInfo.setSubtypes(subtypes);
+
                             maybeStartMonitoringSockets();
-                            // TODO: pass in the subtype as well. Including the subtype in the
-                            // service type would generate service instance names like
-                            // Name._subtype._sub._type._tcp, which is incorrect
-                            // (it should be Name._type._tcp).
                             mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
-                                    typeSubtype.second,
                                     MdnsAdvertisingOptions.newBuilder().build());
                             storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
                                     serviceInfo.getNetwork());
@@ -1388,6 +1432,7 @@
                         servInfo,
                         network == null ? INetd.LOCAL_NET_ID : network.netId,
                         serviceInfo.getInterfaceIndex());
+                servInfo.setSubtypes(dedupSubtypeLabels(serviceInfo.getSubtypes()));
                 return servInfo;
             }
 
@@ -1591,18 +1636,17 @@
     public static Pair<String, String> parseTypeAndSubtype(String serviceType) {
         if (TextUtils.isEmpty(serviceType)) return null;
 
-        final String typeOrSubtypePattern = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
         final Pattern serviceTypePattern = Pattern.compile(
                 // Optional leading subtype (_subtype._type._tcp)
                 // (?: xxx) is a non-capturing parenthesis, don't capture the dot
-                "^(?:(" + typeOrSubtypePattern + ")\\.)?"
+                "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
                         // Actual type (_type._tcp.local)
-                        + "(" + typeOrSubtypePattern + "\\._(?:tcp|udp))"
+                        + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
                         // Drop '.' at the end of service type that is compatible with old backend.
                         // e.g. allow "_type._tcp.local."
                         + "\\.?"
                         // Optional subtype after comma, for "_type._tcp,_subtype" format
-                        + "(?:,(" + typeOrSubtypePattern + "))?"
+                        + "(?:,(" + TYPE_SUBTYPE_LABEL_REGEX + "))?"
                         + "$");
         final Matcher matcher = serviceTypePattern.matcher(serviceType);
         if (!matcher.matches()) return null;
@@ -1611,6 +1655,11 @@
         return new Pair<>(matcher.group(2), subtype);
     }
 
+    /** Returns {@code true} if {@code subtype} is a valid DNS-SD subtype label. */
+    private static boolean checkSubtypeLabel(String subtype) {
+        return Pattern.compile("^" + TYPE_SUBTYPE_LABEL_REGEX + "$").matcher(subtype).matches();
+    }
+
     @VisibleForTesting
     NsdService(Context ctx, Handler handler, long cleanupDelayMs) {
         this(ctx, handler, cleanupDelayMs, new Dependencies());
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
index fc0e11b..135d957 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsAdvertiser.java
@@ -44,6 +44,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
@@ -351,8 +352,7 @@
             mPendingRegistrations.put(id, registration);
             for (int i = 0; i < mAdvertisers.size(); i++) {
                 try {
-                    mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo(),
-                            registration.getSubtype());
+                    mAdvertisers.valueAt(i).addService(id, registration.getServiceInfo());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -367,7 +367,8 @@
         void updateService(int id, @NonNull Registration registration) {
             mPendingRegistrations.put(id, registration);
             for (int i = 0; i < mAdvertisers.size(); i++) {
-                mAdvertisers.valueAt(i).updateService(id, registration.getSubtype());
+                mAdvertisers.valueAt(i).updateService(
+                        id, registration.getServiceInfo().getSubtypes());
             }
         }
 
@@ -417,7 +418,7 @@
                 final Registration registration = mPendingRegistrations.valueAt(i);
                 try {
                     advertiser.addService(mPendingRegistrations.keyAt(i),
-                            registration.getServiceInfo(), registration.getSubtype());
+                            registration.getServiceInfo());
                 } catch (NameConflictException e) {
                     mSharedLog.wtf("Name conflict adding services that should have unique names",
                             e);
@@ -485,16 +486,12 @@
         private int mConflictCount;
         @NonNull
         private NsdServiceInfo mServiceInfo;
-        @Nullable
-        private String mSubtype;
-
         int mConflictDuringProbingCount;
         int mConflictAfterProbingCount;
 
-        private Registration(@NonNull NsdServiceInfo serviceInfo, @Nullable String subtype) {
+        private Registration(@NonNull NsdServiceInfo serviceInfo) {
             this.mOriginalName = serviceInfo.getServiceName();
             this.mServiceInfo = serviceInfo;
-            this.mSubtype = subtype;
         }
 
         /**
@@ -507,10 +504,11 @@
         }
 
         /**
-         * Update subType for the registration.
+         * Update subTypes for the registration.
          */
-        public void updateSubtype(@Nullable String subtype) {
-            this.mSubtype = subtype;
+        public void updateSubtypes(@NonNull Set<String> subtypes) {
+            mServiceInfo = new NsdServiceInfo(mServiceInfo);
+            mServiceInfo.setSubtypes(subtypes);
         }
 
         /**
@@ -540,17 +538,8 @@
             // In case of conflict choose a different service name. After the first conflict use
             // "Name (2)", then "Name (3)" etc.
             // TODO: use a hidden method in NsdServiceInfo once MdnsAdvertiser is moved to service-t
-            final NsdServiceInfo newInfo = new NsdServiceInfo();
+            final NsdServiceInfo newInfo = new NsdServiceInfo(mServiceInfo);
             newInfo.setServiceName(getUpdatedServiceName(renameCount));
-            newInfo.setServiceType(mServiceInfo.getServiceType());
-            for (Map.Entry<String, byte[]> attr : mServiceInfo.getAttributes().entrySet()) {
-                newInfo.setAttribute(attr.getKey(),
-                        attr.getValue() == null ? null : new String(attr.getValue()));
-            }
-            newInfo.setHost(mServiceInfo.getHost());
-            newInfo.setPort(mServiceInfo.getPort());
-            newInfo.setNetwork(mServiceInfo.getNetwork());
-            // interfaceIndex is not set when registering
             return newInfo;
         }
 
@@ -565,11 +554,6 @@
         public NsdServiceInfo getServiceInfo() {
             return mServiceInfo;
         }
-
-        @Nullable
-        public String getSubtype() {
-            return mSubtype;
-        }
     }
 
     /**
@@ -665,14 +649,14 @@
      *
      * @param id A unique ID for the service.
      * @param service The service info to advertise.
-     * @param subtype An optional subtype to advertise the service with.
      * @param advertisingOptions The advertising options.
      */
-    public void addOrUpdateService(int id, NsdServiceInfo service, @Nullable String subtype,
+    public void addOrUpdateService(int id, NsdServiceInfo service,
             MdnsAdvertisingOptions advertisingOptions) {
         checkThread();
         final Registration existingRegistration = mRegistrations.get(id);
         final Network network = service.getNetwork();
+        final Set<String> subtypes = service.getSubtypes();
         Registration registration;
         if (advertisingOptions.isOnlyUpdate()) {
             if (existingRegistration == null) {
@@ -687,10 +671,10 @@
                 return;
 
             }
-            mSharedLog.i("Update service " + service + " with ID " + id + " and subtype " + subtype
-                    + " advertisingOptions " + advertisingOptions);
+            mSharedLog.i("Update service " + service + " with ID " + id + " and subtypes "
+                    + subtypes + " advertisingOptions " + advertisingOptions);
             registration = existingRegistration;
-            registration.updateSubtype(subtype);
+            registration.updateSubtypes(subtypes);
         } else {
             if (existingRegistration != null) {
                 mSharedLog.e("Adding duplicate registration for " + service);
@@ -698,9 +682,9 @@
                 mCb.onRegisterServiceFailed(id, NsdManager.FAILURE_INTERNAL_ERROR);
                 return;
             }
-            mSharedLog.i("Adding service " + service + " with ID " + id + " and subtype " + subtype
-                    + " advertisingOptions " + advertisingOptions);
-            registration = new Registration(service, subtype);
+            mSharedLog.i("Adding service " + service + " with ID " + id + " and subtypes "
+                    + subtypes + " advertisingOptions " + advertisingOptions);
+            registration = new Registration(service);
             final BiPredicate<Network, InterfaceAdvertiserRequest> checkConflictFilter;
             if (network == null) {
                 // If registering on all networks, no advertiser must have conflicts
@@ -793,15 +777,10 @@
     private OffloadServiceInfoWrapper createOffloadService(int serviceId,
             @NonNull Registration registration, byte[] rawOffloadPacket) {
         final NsdServiceInfo nsdServiceInfo = registration.getServiceInfo();
-        final List<String> subTypes = new ArrayList<>();
-        String subType = registration.getSubtype();
-        if (subType != null) {
-            subTypes.add(subType);
-        }
         final OffloadServiceInfo offloadServiceInfo = new OffloadServiceInfo(
                 new OffloadServiceInfo.Key(nsdServiceInfo.getServiceName(),
                         nsdServiceInfo.getServiceType()),
-                subTypes,
+                new ArrayList<>(nsdServiceInfo.getSubtypes()),
                 String.join(".", mDeviceHostName),
                 rawOffloadPacket,
                 // TODO: define overlayable resources in
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index 463df63..aa40c92 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -37,6 +37,7 @@
 import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.util.List;
+import java.util.Set;
 
 /**
  * A class that handles advertising services on a {@link MdnsInterfaceSocket} tied to an interface.
@@ -232,12 +233,12 @@
      * Update an already registered service without sending exit/re-announcement packet.
      *
      * @param id An exiting service id
-     * @param subtype A new subtype
+     * @param subtypes New subtypes
      */
-    public void updateService(int id, @Nullable String subtype) {
+    public void updateService(int id, @NonNull Set<String> subtypes) {
         // The current implementation is intended to be used in cases where subtypes don't get
         // announced.
-        mRecordRepository.updateService(id, subtype);
+        mRecordRepository.updateService(id, subtypes);
     }
 
     /**
@@ -245,9 +246,8 @@
      *
      * @throws NameConflictException There is already a service being advertised with that name.
      */
-    public void addService(int id, NsdServiceInfo service, @Nullable String subtype)
-            throws NameConflictException {
-        final int replacedExitingService = mRecordRepository.addService(id, service, subtype);
+    public void addService(int id, NsdServiceInfo service) throws NameConflictException {
+        final int replacedExitingService = mRecordRepository.addService(id, service);
         // Cancel announcements for the existing service. This only happens for exiting services
         // (so cancelling exiting announcements), as per RecordRepository.addService.
         if (replacedExitingService >= 0) {
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index 48ece68..1909208 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -74,8 +74,6 @@
 
     // Top-level domain for link-local queries, as per RFC6762 3.
     private static final String LOCAL_TLD = "local";
-    // Subtype separator as per RFC6763 7.1 (_printer._sub._http._tcp.local)
-    private static final String SUBTYPE_SEPARATOR = "_sub";
 
     // Service type for service enumeration (RFC6763 9.)
     private static final String[] DNS_SD_SERVICE_TYPE =
@@ -161,8 +159,6 @@
         public final RecordInfo<MdnsTextRecord> txtRecord;
         @NonNull
         public final NsdServiceInfo serviceInfo;
-        @Nullable
-        public final String subtype;
 
         /**
          * Whether the service is sending exit announcements and will be destroyed soon.
@@ -185,28 +181,28 @@
         private boolean isProbing;
 
         /**
-         * Create a ServiceRegistration with only update the subType
+         * Create a ServiceRegistration with only update the subType.
          */
-        ServiceRegistration withSubtype(String newSubType) {
-            return new ServiceRegistration(srvRecord.record.getServiceHost(), serviceInfo,
-                    newSubType, repliedServiceCount, sentPacketCount, exiting, isProbing);
+        ServiceRegistration withSubtypes(@NonNull Set<String> newSubtypes) {
+            NsdServiceInfo newServiceInfo = new NsdServiceInfo(serviceInfo);
+            newServiceInfo.setSubtypes(newSubtypes);
+            return new ServiceRegistration(srvRecord.record.getServiceHost(), newServiceInfo,
+                    repliedServiceCount, sentPacketCount, exiting, isProbing);
         }
 
-
         /**
          * Create a ServiceRegistration for dns-sd service registration (RFC6763).
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                @Nullable String subtype, int repliedServiceCount, int sentPacketCount,
-                boolean exiting, boolean isProbing) {
+                int repliedServiceCount, int sentPacketCount, boolean exiting, boolean isProbing) {
             this.serviceInfo = serviceInfo;
-            this.subtype = subtype;
 
             final String[] serviceType = splitServiceType(serviceInfo);
             final String[] serviceName = splitFullyQualifiedName(serviceInfo, serviceType);
 
-            // Service PTR record
-            final RecordInfo<MdnsPointerRecord> ptrRecord = new RecordInfo<>(
+            // Service PTR records
+            ptrRecords = new ArrayList<>(serviceInfo.getSubtypes().size() + 1);
+            ptrRecords.add(new RecordInfo<>(
                     serviceInfo,
                     new MdnsPointerRecord(
                             serviceType,
@@ -214,26 +210,17 @@
                             false /* cacheFlush */,
                             NON_NAME_RECORDS_TTL_MILLIS,
                             serviceName),
-                    true /* sharedName */);
-
-            if (subtype == null) {
-                this.ptrRecords = Collections.singletonList(ptrRecord);
-            } else {
-                final String[] subtypeName = new String[serviceType.length + 2];
-                System.arraycopy(serviceType, 0, subtypeName, 2, serviceType.length);
-                subtypeName[0] = subtype;
-                subtypeName[1] = SUBTYPE_SEPARATOR;
-                final RecordInfo<MdnsPointerRecord> subtypeRecord = new RecordInfo<>(
-                        serviceInfo,
-                        new MdnsPointerRecord(
-                                subtypeName,
-                                0L /* receiptTimeMillis */,
-                                false /* cacheFlush */,
-                                NON_NAME_RECORDS_TTL_MILLIS,
-                                serviceName),
-                        true /* sharedName */);
-
-                this.ptrRecords = List.of(ptrRecord, subtypeRecord);
+                    true /* sharedName */));
+            for (String subtype : serviceInfo.getSubtypes()) {
+                ptrRecords.add(new RecordInfo<>(
+                    serviceInfo,
+                    new MdnsPointerRecord(
+                            MdnsUtils.constructFullSubtype(serviceType, subtype),
+                            0L /* receiptTimeMillis */,
+                            false /* cacheFlush */,
+                            NON_NAME_RECORDS_TTL_MILLIS,
+                            serviceName),
+                    true /* sharedName */));
             }
 
             srvRecord = new RecordInfo<>(
@@ -284,8 +271,8 @@
          * @param serviceInfo Service to advertise
          */
         ServiceRegistration(@NonNull String[] deviceHostname, @NonNull NsdServiceInfo serviceInfo,
-                @Nullable String subtype, int repliedServiceCount, int sentPacketCount) {
-            this(deviceHostname, serviceInfo, subtype, repliedServiceCount, sentPacketCount,
+                int repliedServiceCount, int sentPacketCount) {
+            this(deviceHostname, serviceInfo,repliedServiceCount, sentPacketCount,
                     false /* exiting */, true /* isProbing */);
         }
 
@@ -328,17 +315,17 @@
      * Update a service that already registered in the repository.
      *
      * @param serviceId An existing service ID.
-     * @param subtype A new subtype
+     * @param subtypes New subtypes
      * @return
      */
-    public void updateService(int serviceId, @Nullable String subtype) {
+    public void updateService(int serviceId, @NonNull Set<String> subtypes) {
         final ServiceRegistration existingRegistration = mServices.get(serviceId);
         if (existingRegistration == null) {
             throw new IllegalArgumentException(
                     "Service ID must already exist for an update request: " + serviceId);
         }
-        final ServiceRegistration updatedRegistration = existingRegistration.withSubtype(
-                subtype);
+        final ServiceRegistration updatedRegistration = existingRegistration.withSubtypes(
+                subtypes);
         mServices.put(serviceId, updatedRegistration);
     }
 
@@ -352,8 +339,7 @@
      *         ID of the replaced service.
      * @throws NameConflictException There is already a (non-exiting) service using the name.
      */
-    public int addService(int serviceId, NsdServiceInfo serviceInfo, @Nullable String subtype)
-            throws NameConflictException {
+    public int addService(int serviceId, NsdServiceInfo serviceInfo) throws NameConflictException {
         if (mServices.contains(serviceId)) {
             throw new IllegalArgumentException(
                     "Service ID must not be reused across registrations: " + serviceId);
@@ -366,7 +352,7 @@
         }
 
         final ServiceRegistration registration = new ServiceRegistration(
-                mDeviceHostname, serviceInfo, subtype, NO_PACKET /* repliedServiceCount */,
+                mDeviceHostname, serviceInfo, NO_PACKET /* repliedServiceCount */,
                 NO_PACKET /* sentPacketCount */);
         mServices.put(serviceId, registration);
 
@@ -929,7 +915,7 @@
         if (existing == null) return null;
 
         final ServiceRegistration newService = new ServiceRegistration(mDeviceHostname, newInfo,
-                existing.subtype, existing.repliedServiceCount, existing.sentPacketCount);
+                existing.repliedServiceCount, existing.sentPacketCount);
         mServices.put(serviceId, newService);
         return makeProbingInfo(
                 serviceId, newService.srvRecord.record, makeProbingInetAddressRecords());
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index 1482ebb..8fc8114 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -233,6 +233,20 @@
                 && mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2;
     }
 
+    /**
+     * Creates a new full subtype name with given service type and subtype labels.
+     *
+     * For example, given ["_http", "_tcp"] and "_printer", this method returns a new String array
+     * of ["_printer", "_sub", "_http", "_tcp"].
+     */
+    public static String[] constructFullSubtype(String[] serviceType, String subtype) {
+        String[] fullSubtype = new String[serviceType.length + 2];
+        fullSubtype[0] = subtype;
+        fullSubtype[1] = MdnsConstants.SUBTYPE_LABEL;
+        System.arraycopy(serviceType, 0, fullSubtype, 2, serviceType.length);
+        return fullSubtype;
+    }
+
     /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
     public static class Clock {
         /**
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
index 44c51d8..ad7a4d7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -151,6 +151,9 @@
     public static final int RTNLGRP_ND_USEROPT = 20;
     public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
 
+    // Netlink family
+    public static final short RTNL_FAMILY_IP6MR = 129;
+
     // Device flags.
     public static final int IFF_UP       = 1 << 0;
     public static final int IFF_LOWER_UP = 1 << 16;
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
index 111e0ba..781a04e 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -142,6 +142,7 @@
                 return (NetlinkMessage) RtNetlinkAddressMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWROUTE:
             case NetlinkConstants.RTM_DELROUTE:
+            case NetlinkConstants.RTM_GETROUTE:
                 return (NetlinkMessage) RtNetlinkRouteMessage.parse(nlmsghdr, byteBuffer);
             case NetlinkConstants.RTM_NEWNEIGH:
             case NetlinkConstants.RTM_DELNEIGH:
diff --git a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
index f1f30d3..f6282fd 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkUtils.java
@@ -29,7 +29,12 @@
 import static android.system.OsConstants.SO_RCVBUF;
 import static android.system.OsConstants.SO_RCVTIMEO;
 import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
 
+import android.net.ParseException;
 import android.net.util.SocketUtils;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -47,7 +52,11 @@
 import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 
 /**
  * Utilities for netlink related class that may not be able to fit into a specific class.
@@ -163,11 +172,7 @@
             Log.e(TAG, errPrefix, e);
             throw new ErrnoException(errPrefix, EIO, e);
         } finally {
-            try {
-                SocketUtils.closeSocket(fd);
-            } catch (IOException e) {
-                // Nothing we can do here
-            }
+            closeSocketQuietly(fd);
         }
     }
 
@@ -308,4 +313,85 @@
     }
 
     private NetlinkUtils() {}
+
+    /**
+     * Sends a netlink dump request and processes the returned dump messages
+     *
+     * @param <T> extends NetlinkMessage
+     * @param dumpRequestMessage netlink dump request message to be sent
+     * @param nlFamily netlink family
+     * @param msgClass expected class of the netlink message
+     * @param func function defined by caller to handle the dump messages
+     * @throws SocketException when fails to create socket
+     * @throws InterruptedIOException when fails to read the dumpFd
+     * @throws ErrnoException when fails to send dump request
+     * @throws ParseException when message can't be parsed
+     */
+    public static <T extends NetlinkMessage> void getAndProcessNetlinkDumpMessages(
+            byte[] dumpRequestMessage, int nlFamily, Class<T> msgClass,
+            Consumer<T> func)
+            throws SocketException, InterruptedIOException, ErrnoException, ParseException {
+        // Create socket and send dump request
+        final FileDescriptor fd;
+        try {
+            fd = netlinkSocketForProto(nlFamily);
+        } catch (ErrnoException  e) {
+            Log.e(TAG, "Failed to create netlink socket " + e);
+            throw e.rethrowAsSocketException();
+        }
+
+        try {
+            connectToKernel(fd);
+        } catch (ErrnoException | SocketException e) {
+            Log.e(TAG, "Failed to connect netlink socket to kernel " + e);
+            closeSocketQuietly(fd);
+            return;
+        }
+
+        try {
+            sendMessage(fd, dumpRequestMessage, 0, dumpRequestMessage.length, IO_TIMEOUT_MS);
+        } catch (InterruptedIOException | ErrnoException e) {
+            Log.e(TAG, "Failed to send dump request " + e);
+            closeSocketQuietly(fd);
+            throw e;
+        }
+
+        while (true) {
+            final ByteBuffer buf = recvMessage(
+                    fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, IO_TIMEOUT_MS);
+
+            while (buf.remaining() > 0) {
+                final int position = buf.position();
+                final NetlinkMessage nlMsg = NetlinkMessage.parse(buf, nlFamily);
+                if (nlMsg == null) {
+                    // Move to the position where parse started for error log.
+                    buf.position(position);
+                    Log.e(TAG, "Failed to parse netlink message: " + hexify(buf));
+                    closeSocketQuietly(fd);
+                    throw new ParseException("Failed to parse netlink message");
+                }
+
+                if (nlMsg.getHeader().nlmsg_type == NLMSG_DONE) {
+                    closeSocketQuietly(fd);
+                    return;
+                }
+
+                if (!msgClass.isInstance(nlMsg)) {
+                    Log.e(TAG, "Received unexpected netlink message: " + nlMsg);
+                    continue;
+                }
+
+                final T msg = (T) nlMsg;
+                func.accept(msg);
+            }
+        }
+    }
+
+    private static void closeSocketQuietly(final FileDescriptor fd) {
+        try {
+            SocketUtils.closeSocket(fd);
+        } catch (IOException e) {
+            // Nothing we can do here
+        }
+    }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
index 9acac69..b2b1e93 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -19,11 +19,15 @@
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
 
+import static android.system.OsConstants.NETLINK_ROUTE;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+import static com.android.net.module.util.netlink.NetlinkConstants.hexify;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
 
 import android.annotation.SuppressLint;
 import android.net.IpPrefix;
+import android.net.RouteInfo;
 import android.system.OsConstants;
 
 import androidx.annotation.NonNull;
@@ -34,6 +38,9 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.util.Arrays;
 
 /**
  * A NetlinkMessage subclass for rtnetlink route messages.
@@ -49,31 +56,69 @@
  */
 public class RtNetlinkRouteMessage extends NetlinkMessage {
     public static final short RTA_DST           = 1;
+    public static final short RTA_SRC           = 2;
+    public static final short RTA_IIF           = 3;
     public static final short RTA_OIF           = 4;
     public static final short RTA_GATEWAY       = 5;
     public static final short RTA_CACHEINFO     = 12;
+    public static final short RTA_EXPIRES       = 23;
 
-    private int mIfindex;
+    public static final short RTNH_F_UNRESOLVED = 32;   // The multicast route is unresolved
+
+    public static final String TAG = "NetlinkRouteMessage";
+
+    // For multicast routes, whether the route is resolved or unresolved
+    private boolean mIsResolved;
+    // The interface index for incoming interface, this is set for multicast
+    // routes, see common/net/ipv4/ipmr_base.c mr_fill_mroute
+    private int mIifIndex; // Incoming interface of a route, for resolved multicast routes
+    private int mOifIndex;
     @NonNull
     private StructRtMsg mRtmsg;
-    @NonNull
-    private IpPrefix mDestination;
+    @Nullable
+    private IpPrefix mSource; // Source address of a route, for all multicast routes
+    @Nullable
+    private IpPrefix mDestination; // Destination of a route, can be null for RTM_GETROUTE
     @Nullable
     private InetAddress mGateway;
     @Nullable
     private StructRtaCacheInfo mRtaCacheInfo;
+    private long mSinceLastUseMillis; // Milliseconds since the route was used,
+                                      // for resolved multicast routes
 
-    private RtNetlinkRouteMessage(StructNlMsgHdr header) {
+    public RtNetlinkRouteMessage(StructNlMsgHdr header, StructRtMsg rtMsg) {
         super(header);
-        mRtmsg = null;
+        mRtmsg = rtMsg;
+        mSource = null;
         mDestination = null;
         mGateway = null;
-        mIfindex = 0;
+        mIifIndex = 0;
+        mOifIndex = 0;
         mRtaCacheInfo = null;
+        mSinceLastUseMillis = -1;
+    }
+
+    /**
+     * Returns the rtnetlink family.
+     */
+    public short getRtmFamily() {
+        return mRtmsg.family;
+    }
+
+    /**
+     * Returns if the route is resolved. This is always true for unicast,
+     * and may be false only for multicast routes.
+     */
+    public boolean isResolved() {
+        return mIsResolved;
+    }
+
+    public int getIifIndex() {
+        return mIifIndex;
     }
 
     public int getInterfaceIndex() {
-        return mIfindex;
+        return mOifIndex;
     }
 
     @NonNull
@@ -86,6 +131,14 @@
         return mDestination;
     }
 
+    /**
+     * Get source address of a route. This is for multicast routes.
+     */
+    @NonNull
+    public IpPrefix getSource() {
+        return mSource;
+    }
+
     @Nullable
     public InetAddress getGateway() {
         return mGateway;
@@ -97,6 +150,18 @@
     }
 
     /**
+     * RTA_EXPIRES attribute returned by kernel to indicate the clock ticks
+     * from the route was last used to now, converted to milliseconds.
+     * This is set for multicast routes.
+     *
+     * Note that this value is not updated with the passage of time. It always
+     * returns the value that was read when the netlink message was parsed.
+     */
+    public long getSinceLastUseMillis() {
+        return mSinceLastUseMillis;
+    }
+
+    /**
      * Check whether the address families of destination and gateway match rtm_family in
      * StructRtmsg.
      *
@@ -107,7 +172,8 @@
     private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
             int family) {
         return ((address instanceof Inet4Address) && (family == AF_INET))
-                || ((address instanceof Inet6Address) && (family == AF_INET6));
+                || ((address instanceof Inet6Address) &&
+                        (family == AF_INET6 || family == RTNL_FAMILY_IP6MR));
     }
 
     /**
@@ -121,11 +187,11 @@
     @Nullable
     public static RtNetlinkRouteMessage parse(@NonNull final StructNlMsgHdr header,
             @NonNull final ByteBuffer byteBuffer) {
-        final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header);
-
-        routeMsg.mRtmsg = StructRtMsg.parse(byteBuffer);
-        if (routeMsg.mRtmsg == null) return null;
+        final StructRtMsg rtmsg = StructRtMsg.parse(byteBuffer);
+        if (rtmsg == null) return null;
+        final RtNetlinkRouteMessage routeMsg = new RtNetlinkRouteMessage(header, rtmsg);
         int rtmFamily = routeMsg.mRtmsg.family;
+        routeMsg.mIsResolved = ((routeMsg.mRtmsg.flags & RTNH_F_UNRESOLVED) == 0);
 
         // RTA_DST
         final int baseOffset = byteBuffer.position();
@@ -139,12 +205,24 @@
             routeMsg.mDestination = new IpPrefix(destination, routeMsg.mRtmsg.dstLen);
         } else if (rtmFamily == AF_INET) {
             routeMsg.mDestination = new IpPrefix(IPV4_ADDR_ANY, 0);
-        } else if (rtmFamily == AF_INET6) {
+        } else if (rtmFamily == AF_INET6 || rtmFamily == RTNL_FAMILY_IP6MR) {
             routeMsg.mDestination = new IpPrefix(IPV6_ADDR_ANY, 0);
         } else {
             return null;
         }
 
+        // RTA_SRC
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_SRC, byteBuffer);
+        if (nlAttr != null) {
+            final InetAddress source = nlAttr.getValueAsInetAddress();
+            // If the RTA_SRC attribute is malformed, return null.
+            if (source == null) return null;
+            // If the address family of destination doesn't match rtm_family, return null.
+            if (!matchRouteAddressFamily(source, rtmFamily)) return null;
+            routeMsg.mSource = new IpPrefix(source, routeMsg.mRtmsg.srcLen);
+        }
+
         // RTA_GATEWAY
         byteBuffer.position(baseOffset);
         nlAttr = StructNlAttr.findNextAttrOfType(RTA_GATEWAY, byteBuffer);
@@ -156,6 +234,17 @@
             if (!matchRouteAddressFamily(routeMsg.mGateway, rtmFamily)) return null;
         }
 
+        // RTA_IIF
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_IIF, byteBuffer);
+        if (nlAttr != null) {
+            Integer iifInteger = nlAttr.getValueAsInteger();
+            if (iifInteger == null) {
+                return null;
+            }
+            routeMsg.mIifIndex = iifInteger;
+        }
+
         // RTA_OIF
         byteBuffer.position(baseOffset);
         nlAttr = StructNlAttr.findNextAttrOfType(RTA_OIF, byteBuffer);
@@ -164,7 +253,7 @@
             // the interface index to a name themselves. This may not succeed or may be
             // incorrect, because the interface might have been deleted, or even deleted
             // and re-added with a different index, since the netlink message was sent.
-            routeMsg.mIfindex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
+            routeMsg.mOifIndex = nlAttr.getValueAsInt(0 /* 0 isn't a valid ifindex */);
         }
 
         // RTA_CACHEINFO
@@ -174,33 +263,59 @@
             routeMsg.mRtaCacheInfo = StructRtaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
         }
 
+        // RTA_EXPIRES
+        byteBuffer.position(baseOffset);
+        nlAttr = StructNlAttr.findNextAttrOfType(RTA_EXPIRES, byteBuffer);
+        if (nlAttr != null) {
+            final Long sinceLastUseCentis = nlAttr.getValueAsLong();
+            // If the RTA_EXPIRES attribute is malformed, return null.
+            if (sinceLastUseCentis == null) return null;
+            // RTA_EXPIRES returns time in clock ticks of USER_HZ(100), which is centiseconds
+            routeMsg.mSinceLastUseMillis = sinceLastUseCentis * 10;
+        }
+
         return routeMsg;
     }
 
     /**
      * Write a rtnetlink address message to {@link ByteBuffer}.
      */
-    @VisibleForTesting
-    protected void pack(ByteBuffer byteBuffer) {
+    public void pack(ByteBuffer byteBuffer) {
         getHeader().pack(byteBuffer);
         mRtmsg.pack(byteBuffer);
 
-        final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
-        destination.pack(byteBuffer);
+        if (mSource != null) {
+            final StructNlAttr source = new StructNlAttr(RTA_SRC, mSource.getAddress());
+            source.pack(byteBuffer);
+        }
+
+        if (mDestination != null) {
+            final StructNlAttr destination = new StructNlAttr(RTA_DST, mDestination.getAddress());
+            destination.pack(byteBuffer);
+        }
 
         if (mGateway != null) {
             final StructNlAttr gateway = new StructNlAttr(RTA_GATEWAY, mGateway.getAddress());
             gateway.pack(byteBuffer);
         }
-        if (mIfindex != 0) {
-            final StructNlAttr ifindex = new StructNlAttr(RTA_OIF, mIfindex);
-            ifindex.pack(byteBuffer);
+        if (mIifIndex != 0) {
+            final StructNlAttr iifindex = new StructNlAttr(RTA_IIF, mIifIndex);
+            iifindex.pack(byteBuffer);
+        }
+        if (mOifIndex != 0) {
+            final StructNlAttr oifindex = new StructNlAttr(RTA_OIF, mOifIndex);
+            oifindex.pack(byteBuffer);
         }
         if (mRtaCacheInfo != null) {
             final StructNlAttr cacheInfo = new StructNlAttr(RTA_CACHEINFO,
                     mRtaCacheInfo.writeToBytes());
             cacheInfo.pack(byteBuffer);
         }
+        if (mSinceLastUseMillis >= 0) {
+            final long sinceLastUseCentis = mSinceLastUseMillis / 10;
+            final StructNlAttr expires = new StructNlAttr(RTA_EXPIRES, sinceLastUseCentis);
+            expires.pack(byteBuffer);
+        }
     }
 
     @Override
@@ -208,10 +323,14 @@
         return "RtNetlinkRouteMessage{ "
                 + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
                 + "Rtmsg{" + mRtmsg.toString() + "}, "
-                + "destination{" + mDestination.getAddress().getHostAddress() + "}, "
+                + (mSource == null ? "" : "source{" + mSource.getAddress().getHostAddress() + "}, ")
+                + (mDestination == null ?
+                        "" : "destination{" + mDestination.getAddress().getHostAddress() + "}, ")
                 + "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
-                + "ifindex{" + mIfindex + "}, "
+                + (mIifIndex == 0 ? "" : "iifindex{" + mIifIndex + "}, ")
+                + "oifindex{" + mOifIndex + "}, "
                 + "rta_cacheinfo{" + (mRtaCacheInfo == null ? "" : mRtaCacheInfo.toString()) + "} "
+                + (mSinceLastUseMillis < 0 ? "" : "sinceLastUseMillis{" + mSinceLastUseMillis + "}")
                 + "}";
     }
 }
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
index a9b6495..43e8312 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlAttr.java
@@ -21,6 +21,8 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import static java.nio.ByteOrder.nativeOrder;
+
 import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -152,12 +154,12 @@
         nla_type = type;
         setValue(new byte[Short.BYTES]);
         final ByteBuffer buf = getValueAsByteBuffer();
-        final ByteOrder originalOrder = buf.order();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
         try {
             buf.order(order);
             buf.putShort(value);
         } finally {
-            buf.order(originalOrder);
+            buf.order(nativeOrder());
         }
     }
 
@@ -169,12 +171,29 @@
         nla_type = type;
         setValue(new byte[Integer.BYTES]);
         final ByteBuffer buf = getValueAsByteBuffer();
-        final ByteOrder originalOrder = buf.order();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
         try {
             buf.order(order);
             buf.putInt(value);
         } finally {
-            buf.order(originalOrder);
+            buf.order(nativeOrder());
+        }
+    }
+
+    public StructNlAttr(short type, long value) {
+        this(type, value, ByteOrder.nativeOrder());
+    }
+
+    public StructNlAttr(short type, long value, ByteOrder order) {
+        nla_type = type;
+        setValue(new byte[Long.BYTES]);
+        final ByteBuffer buf = getValueAsByteBuffer();
+        // ByteBuffer returned by getValueAsByteBuffer is always in native byte order.
+        try {
+            buf.order(order);
+            buf.putLong(value);
+        } finally {
+            buf.order(nativeOrder());
         }
     }
 
@@ -288,6 +307,7 @@
 
     /**
      * Get attribute value as Integer, or null if malformed (e.g., length is not 4 bytes).
+     * The attribute value is assumed to be in native byte order.
      */
     public Integer getValueAsInteger() {
         final ByteBuffer byteBuffer = getValueAsByteBuffer();
@@ -298,6 +318,18 @@
     }
 
     /**
+     * Get attribute value as Long, or null if malformed (e.g., length is not 8 bytes).
+     * The attribute value is assumed to be in native byte order.
+     */
+    public Long getValueAsLong() {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Long.BYTES) {
+            return null;
+        }
+        return byteBuffer.getLong();
+    }
+
+    /**
      * Get attribute value as Int, default value if malformed.
      */
     public int getValueAsInt(int defaultValue) {
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
index ff37639..5272366 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructNlMsgHdr.java
@@ -71,13 +71,17 @@
             }
             sb.append("NLM_F_ECHO");
         }
-        if ((flags & NLM_F_ROOT) != 0) {
+        if ((flags & NLM_F_DUMP) == NLM_F_DUMP) {
+            if (sb.length() > 0) {
+                sb.append("|");
+            }
+            sb.append("NLM_F_DUMP");
+        } else if ((flags & NLM_F_ROOT) != 0) { // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
             if (sb.length() > 0) {
                 sb.append("|");
             }
             sb.append("NLM_F_ROOT");
-        }
-        if ((flags & NLM_F_MATCH) != 0) {
+        } else if ((flags & NLM_F_MATCH) != 0) {
             if (sb.length() > 0) {
                 sb.append("|");
             }
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 031e52f..0dfca57 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -33,7 +33,10 @@
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
         "//packages/modules/NetworkStack/tests/integration",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        test: true
+    },
 }
 
 android_test {
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
index 5a231fc..17d4e81 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/NetlinkUtilsTest.java
@@ -55,6 +55,9 @@
 import java.nio.ByteOrder;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -65,19 +68,14 @@
 
     @Test
     public void testGetNeighborsQuery() throws Exception {
-        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
-        assertNotNull(fd);
-
-        NetlinkUtils.connectToKernel(fd);
-
-        final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
-        assertNotNull(localAddr);
-        assertEquals(0, localAddr.getGroupsMask());
-        assertTrue(0 != localAddr.getPortId());
-
         final byte[] req = RtNetlinkNeighborMessage.newGetNeighborsRequest(TEST_SEQNO);
         assertNotNull(req);
 
+        List<RtNetlinkNeighborMessage> msgs = new ArrayList<>();
+        Consumer<RtNetlinkNeighborMessage> handleNlDumpMsg = (msg) -> {
+            msgs.add(msg);
+        };
+
         final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
         final int targetSdk =
                 ctx.getPackageManager()
@@ -94,7 +92,8 @@
             assumeFalse("network_stack context is expected to have permission to send RTM_GETNEIGH",
                     ctxt.startsWith("u:r:network_stack:s0"));
             try {
-                NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS);
+                NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+                        NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
                 fail("RTM_GETNEIGH is not allowed for apps targeting SDK > 31 on T+ platforms,"
                         + " target SDK version: " + targetSdk);
             } catch (ErrnoException e) {
@@ -105,106 +104,70 @@
         }
 
         // Check that apps targeting lower API levels / running on older platforms succeed
-        assertEquals(req.length,
-                NetlinkUtils.sendMessage(fd, req, 0, req.length, TEST_TIMEOUT_MS));
+        NetlinkUtils.<RtNetlinkNeighborMessage>getAndProcessNetlinkDumpMessages(req,
+                NETLINK_ROUTE, RtNetlinkNeighborMessage.class, handleNlDumpMsg);
 
-        int neighMessageCount = 0;
-        int doneMessageCount = 0;
-
-        while (doneMessageCount == 0) {
-            ByteBuffer response =
-                    NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TEST_TIMEOUT_MS);
-            assertNotNull(response);
-            assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
-            assertEquals(0, response.position());
-            assertEquals(ByteOrder.nativeOrder(), response.order());
-
-            // Verify the messages at least appears minimally reasonable.
-            while (response.remaining() > 0) {
-                final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
-                assertNotNull(msg);
-                final StructNlMsgHdr hdr = msg.getHeader();
-                assertNotNull(hdr);
-
-                if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
-                    doneMessageCount++;
-                    continue;
-                }
-
-                assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
-                assertTrue(msg instanceof RtNetlinkNeighborMessage);
-                assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
-                assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
-                assertEquals(localAddr.getPortId(), hdr.nlmsg_pid);
-
-                neighMessageCount++;
-            }
+        for (var msg : msgs) {
+            assertNotNull(msg);
+            final StructNlMsgHdr hdr = msg.getHeader();
+            assertNotNull(hdr);
+            assertEquals(NetlinkConstants.RTM_NEWNEIGH, hdr.nlmsg_type);
+            assertTrue((hdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
+            assertEquals(TEST_SEQNO, hdr.nlmsg_seq);
         }
 
-        assertEquals(1, doneMessageCount);
         // TODO: make sure this test passes sanely in airplane mode.
-        assertTrue(neighMessageCount > 0);
-
-        IoUtils.closeQuietly(fd);
+        assertTrue(msgs.size() > 0);
     }
 
     @Test
     public void testBasicWorkingGetAddrQuery() throws Exception {
-        final FileDescriptor fd = NetlinkUtils.netlinkSocketForProto(NETLINK_ROUTE);
-        assertNotNull(fd);
-
-        NetlinkUtils.connectToKernel(fd);
-
-        final NetlinkSocketAddress localAddr = (NetlinkSocketAddress) Os.getsockname(fd);
-        assertNotNull(localAddr);
-        assertEquals(0, localAddr.getGroupsMask());
-        assertTrue(0 != localAddr.getPortId());
-
         final int testSeqno = 8;
         final byte[] req = newGetAddrRequest(testSeqno);
         assertNotNull(req);
 
-        final long timeout = 500;
-        assertEquals(req.length, NetlinkUtils.sendMessage(fd, req, 0, req.length, timeout));
+        List<RtNetlinkAddressMessage> msgs = new ArrayList<>();
+        Consumer<RtNetlinkAddressMessage> handleNlDumpMsg = (msg) -> {
+            msgs.add(msg);
+        };
+        NetlinkUtils.<RtNetlinkAddressMessage>getAndProcessNetlinkDumpMessages(req, NETLINK_ROUTE,
+                RtNetlinkAddressMessage.class, handleNlDumpMsg);
 
-        int addrMessageCount = 0;
+        boolean ipv4LoopbackAddressFound = false;
+        boolean ipv6LoopbackAddressFound = false;
+        final InetAddress loopbackIpv4 = InetAddress.getByName("127.0.0.1");
+        final InetAddress loopbackIpv6 = InetAddress.getByName("::1");
 
-        while (true) {
-            ByteBuffer response = NetlinkUtils.recvMessage(fd, DEFAULT_RECV_BUFSIZE, timeout);
-            assertNotNull(response);
-            assertTrue(StructNlMsgHdr.STRUCT_SIZE <= response.limit());
-            assertEquals(0, response.position());
-            assertEquals(ByteOrder.nativeOrder(), response.order());
-
-            final NetlinkMessage msg = NetlinkMessage.parse(response, NETLINK_ROUTE);
+        for (var msg : msgs) {
             assertNotNull(msg);
             final StructNlMsgHdr nlmsghdr = msg.getHeader();
             assertNotNull(nlmsghdr);
-
-            if (nlmsghdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
-                break;
-            }
-
             assertEquals(NetlinkConstants.RTM_NEWADDR, nlmsghdr.nlmsg_type);
             assertTrue((nlmsghdr.nlmsg_flags & StructNlMsgHdr.NLM_F_MULTI) != 0);
             assertEquals(testSeqno, nlmsghdr.nlmsg_seq);
-            assertEquals(localAddr.getPortId(), nlmsghdr.nlmsg_pid);
             assertTrue(msg instanceof RtNetlinkAddressMessage);
-            addrMessageCount++;
-
-            // From the query response we can see the RTM_NEWADDR messages representing for IPv4
-            // and IPv6 loopback address: 127.0.0.1 and ::1.
+            // When parsing the full response we can see the RTM_NEWADDR messages representing for
+            // IPv4 and IPv6 loopback address: 127.0.0.1 and ::1 and non-loopback addresses.
             final StructIfaddrMsg ifaMsg = ((RtNetlinkAddressMessage) msg).getIfaddrHeader();
             final InetAddress ipAddress = ((RtNetlinkAddressMessage) msg).getIpAddress();
             assertTrue(
                     "Non-IP address family: " + ifaMsg.family,
                     ifaMsg.family == AF_INET || ifaMsg.family == AF_INET6);
-            assertTrue(ipAddress.isLoopbackAddress());
+            assertNotNull(ipAddress);
+
+            if (ipAddress.equals(loopbackIpv4)) {
+                ipv4LoopbackAddressFound = true;
+                assertTrue(ipAddress.isLoopbackAddress());
+            }
+            if (ipAddress.equals(loopbackIpv6)) {
+                ipv6LoopbackAddressFound = true;
+                assertTrue(ipAddress.isLoopbackAddress());
+            }
         }
 
-        assertTrue(addrMessageCount > 0);
-
-        IoUtils.closeQuietly(fd);
+        assertTrue(msgs.size() > 0);
+        // Check ipv4 and ipv6 loopback addresses are in the output
+        assertTrue(ipv4LoopbackAddressFound && ipv6LoopbackAddressFound);
     }
 
     /** A convenience method to create an RTM_GETADDR request message. */
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
index 9881653..50b8278 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -16,7 +16,9 @@
 
 package com.android.net.module.util.netlink;
 
+import static android.system.OsConstants.AF_INET6;
 import static android.system.OsConstants.NETLINK_ROUTE;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTNL_FAMILY_IP6MR;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -38,6 +40,7 @@
 import java.net.Inet6Address;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Arrays;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -127,6 +130,72 @@
         assertEquals(RTM_NEWROUTE_PACK_HEX, HexDump.toHexString(packBuffer.array()));
     }
 
+    private static final String RTM_GETROUTE_MULTICAST_IPV6_HEX =
+            "1C0000001A0001030000000000000000"             // struct nlmsghr
+            + "810000000000000000000000";                  // struct rtmsg
+
+    private static final String RTM_NEWROUTE_MULTICAST_IPV6_HEX =
+            "88000000180002000000000000000000"             // struct nlmsghr
+            + "81808000FE11000500000000"                   // struct rtmsg
+            + "08000F00FE000000"                           // RTA_TABLE
+            + "14000200FDACC0F1DBDB000195B7C1A464F944EA"   // RTA_SRC
+            + "14000100FF040000000000000000000000001234"   // RTA_DST
+            + "0800030014000000"                           // RTA_IIF
+            + "0C0009000800000111000000"                   // RTA_MULTIPATH
+            + "1C00110001000000000000009400000000000000"   // RTA_STATS
+            + "0000000000000000"
+            + "0C0017007617000000000000";                  // RTA_EXPIRES
+
+    @Test
+    public void testParseRtmNewRoute_MulticastIpv6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final StructNlMsgHdr hdr = routeMsg.getHeader();
+        assertNotNull(hdr);
+        assertEquals(136, hdr.nlmsg_len);
+        assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+
+        final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+        assertNotNull(rtmsg);
+        assertEquals((byte) 129, (byte) rtmsg.family);
+        assertEquals(128, rtmsg.dstLen);
+        assertEquals(128, rtmsg.srcLen);
+        assertEquals(0xFE, rtmsg.table);
+
+        assertEquals(routeMsg.getSource(),
+                new IpPrefix("fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea/128"));
+        assertEquals(routeMsg.getDestination(), new IpPrefix("ff04::1234/128"));
+        assertEquals(20, routeMsg.getIifIndex());
+        assertEquals(60060, routeMsg.getSinceLastUseMillis());
+    }
+
+    // NEWROUTE message for multicast IPv6 with the packed attributes
+    private static final String RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX =
+            "58000000180002000000000000000000"             // struct nlmsghr
+            + "81808000FE11000500000000"                   // struct rtmsg
+            + "14000200FDACC0F1DBDB000195B7C1A464F944EA"   // RTA_SRC
+            + "14000100FF040000000000000000000000001234"   // RTA_DST
+            + "0800030014000000"                           // RTA_IIF
+            + "0C0017007617000000000000";                  // RTA_EXPIRES
+    @Test
+    public void testPackRtmNewRoute_MulticastIpv6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        final ByteBuffer packBuffer = ByteBuffer.allocate(88);
+        packBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        routeMsg.pack(packBuffer);
+        assertEquals(RTM_NEWROUTE_MULTICAST_IPV6_PACK_HEX,
+                HexDump.toHexString(packBuffer.array()));
+    }
+
     private static final String RTM_NEWROUTE_TRUNCATED_HEX =
             "48000000180000060000000000000000"             // struct nlmsghr
             + "0A400000FC02000100000000"                   // struct rtmsg
@@ -220,10 +289,79 @@
                 + "scope: 0, type: 1, flags: 0}, "
                 + "destination{2001:db8:1::}, "
                 + "gateway{fe80::1}, "
-                + "ifindex{735}, "
+                + "oifindex{735}, "
                 + "rta_cacheinfo{clntref: 0, lastuse: 0, expires: 59998, error: 0, used: 0, "
                 + "id: 0, ts: 0, tsage: 0} "
                 + "}";
         assertEquals(expected, routeMsg.toString());
     }
+
+    @Test
+    public void testToString_RtmGetRoute() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_GETROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final String expected = "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{28}, nlmsg_type{26(RTM_GETROUTE)}, "
+                + "nlmsg_flags{769(NLM_F_REQUEST|NLM_F_DUMP)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Rtmsg{"
+                + "family: 129, dstLen: 0, srcLen: 0, tos: 0, table: 0, protocol: 0, "
+                + "scope: 0, type: 0, flags: 0}, "
+                + "destination{::}, "
+                + "gateway{}, "
+                + "oifindex{0}, "
+                + "rta_cacheinfo{} "
+                + "}";
+        assertEquals(expected, routeMsg.toString());
+    }
+
+    @Test
+    public void testToString_RtmNewRouteMulticastIpv6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        final String expected = "RtNetlinkRouteMessage{ "
+                + "nlmsghdr{"
+                + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+                + "nlmsg_flags{2(NLM_F_MULTI)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+                + "Rtmsg{"
+                + "family: 129, dstLen: 128, srcLen: 128, tos: 0, table: 254, protocol: 17, "
+                + "scope: 0, type: 5, flags: 0}, "
+                + "source{fdac:c0f1:dbdb:1:95b7:c1a4:64f9:44ea}, "
+                + "destination{ff04::1234}, "
+                + "gateway{}, "
+                + "iifindex{20}, "
+                + "oifindex{0}, "
+                + "rta_cacheinfo{} "
+                + "sinceLastUseMillis{60060}"
+                + "}";
+        assertEquals(expected, routeMsg.toString());
+    }
+
+    @Test
+    public void testGetRtmFamily_RTNL_FAMILY_IP6MR() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_MULTICAST_IPV6_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        assertEquals(RTNL_FAMILY_IP6MR, routeMsg.getRtmFamily());
+    }
+
+    @Test
+    public void testGetRtmFamily_AF_INET6() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+        final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+
+        assertEquals(AF_INET6, routeMsg.getRtmFamily());
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
index af3fac2..4c3fde6 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlAttrTest.java
@@ -92,4 +92,26 @@
         assertNull(integer3);
         assertEquals(int3, 0x08 /* default value */);
     }
+
+    @Test
+    public void testGetValueAsLong() {
+        final Long input = 1234567L;
+        // Not a real netlink attribute, just for testing
+        final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+        final Long output = attr.getValueAsLong();
+
+        assertEquals(input, output);
+    }
+
+    @Test
+    public void testGetValueAsLong_malformed() {
+        final int input = 1234567;
+        // Not a real netlink attribute, just for testing
+        final StructNlAttr attr = new StructNlAttr(IFA_FLAGS, input);
+
+        final Long output = attr.getValueAsLong();
+
+        assertNull(output);
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
index b7f68c6..a0d8b8c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/StructNlMsgHdrTest.java
@@ -16,6 +16,7 @@
 
 package com.android.net.module.util.netlink;
 
+import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_DUMP;
 import static org.junit.Assert.fail;
 
 import android.system.OsConstants;
@@ -48,10 +49,14 @@
     public static final String TEST_NLMSG_PID_STR = "nlmsg_pid{5678}";
 
     private StructNlMsgHdr makeStructNlMsgHdr(short type) {
+        return makeStructNlMsgHdr(type, TEST_NLMSG_FLAGS);
+    }
+
+    private StructNlMsgHdr makeStructNlMsgHdr(short type, short flags) {
         final StructNlMsgHdr struct = new StructNlMsgHdr();
         struct.nlmsg_len = TEST_NLMSG_LEN;
         struct.nlmsg_type = type;
-        struct.nlmsg_flags = TEST_NLMSG_FLAGS;
+        struct.nlmsg_flags = flags;
         struct.nlmsg_seq = TEST_NLMSG_SEQ;
         struct.nlmsg_pid = TEST_NLMSG_PID;
         return struct;
@@ -62,6 +67,11 @@
         fail("\"" + actualValue + "\" does not contain \"" + expectedSubstring + "\"");
     }
 
+    private static void assertNotContains(String actualValue, String unexpectedSubstring) {
+        if (!actualValue.contains(unexpectedSubstring)) return;
+        fail("\"" + actualValue + "\" contains \"" + unexpectedSubstring + "\"");
+    }
+
     @Test
     public void testToString() {
         StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_NEWADDR);
@@ -99,4 +109,31 @@
         assertContains(s, TEST_NLMSG_PID_STR);
         assertContains(s, "nlmsg_type{20(SOCK_DIAG_BY_FAMILY)}");
     }
+
+    @Test
+    public void testToString_flags_dumpRequest() {
+        final short flags = StructNlMsgHdr.NLM_F_REQUEST | StructNlMsgHdr.NLM_F_DUMP;
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+        assertContains(s, "RTM_GETROUTE");
+        assertContains(s, "NLM_F_REQUEST");
+        assertContains(s, "NLM_F_DUMP");
+        // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+        assertNotContains(s, "NLM_F_MATCH");
+        assertNotContains(s, "NLM_F_ROOT");
+    }
+
+    @Test
+    public void testToString_flags_root() {
+        final short flags = StructNlMsgHdr.NLM_F_ROOT;
+        StructNlMsgHdr struct = makeStructNlMsgHdr(NetlinkConstants.RTM_GETROUTE, flags);
+
+        String s = struct.toString(OsConstants.NETLINK_ROUTE);
+
+        assertContains(s, "NLM_F_ROOT");
+        // NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH;
+        assertNotContains(s, "NLM_F_DUMP");
+    }
 }
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index a5e5afb..a5c4fea 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -84,6 +84,13 @@
         "host/**/*.kt",
     ],
     libs: ["tradefed"],
-    test_suites: ["ats", "device-tests", "general-tests", "cts", "mts-networking"],
+    test_suites: [
+        "ats",
+        "device-tests",
+        "general-tests",
+        "cts",
+        "mts-networking",
+        "mcts-networking",
+    ],
     data: [":ConnectivityTestPreparer"],
 }
diff --git a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
index ffe0e91..79c4980 100644
--- a/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/common/java/android/net/nsd/NsdServiceInfoTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -40,6 +41,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
@@ -114,6 +116,7 @@
         NsdServiceInfo fullInfo = new NsdServiceInfo();
         fullInfo.setServiceName("kitten");
         fullInfo.setServiceType("_kitten._tcp");
+        fullInfo.setSubtypes(Set.of("_thread", "_matter"));
         fullInfo.setPort(4242);
         fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
         fullInfo.setNetwork(new Network(123));
@@ -149,7 +152,7 @@
         assertFalse(attributedInfo.getAttributes().keySet().contains("sticky"));
     }
 
-    public void checkParcelable(NsdServiceInfo original) {
+    private static void checkParcelable(NsdServiceInfo original) {
         // Write to parcel.
         Parcel p = Parcel.obtain();
         Bundle writer = new Bundle();
@@ -179,11 +182,20 @@
         }
     }
 
-    public void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
+    private static void assertEmptyServiceInfo(NsdServiceInfo shouldBeEmpty) {
         byte[] txtRecord = shouldBeEmpty.getTxtRecord();
         if (txtRecord == null || txtRecord.length == 0) {
             return;
         }
         fail("NsdServiceInfo.getTxtRecord did not return null but " + Arrays.toString(txtRecord));
     }
+
+    @Test
+    public void testSubtypesValidSubtypesSuccess() {
+        NsdServiceInfo info = new NsdServiceInfo();
+
+        info.setSubtypes(Set.of("_thread", "_matter"));
+
+        assertEquals(Set.of("_thread", "_matter"), info.getSubtypes());
+    }
 }
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 9310888..3d53d6c 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -131,6 +131,7 @@
         "cts",
         "general-tests",
         "mts-tethering",
+        "mcts-tethering",
     ],
 }
 
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e1ea2b9..e0387c9 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -43,6 +43,7 @@
 import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.DiscoveryStopped
 import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceFound
 import android.net.cts.NsdDiscoveryRecord.DiscoveryEvent.ServiceLost
+import android.net.cts.NsdRegistrationRecord.RegistrationEvent.RegistrationFailed
 import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceRegistered
 import android.net.cts.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
 import android.net.cts.NsdResolveRecord.ResolveEvent.ResolutionStopped
@@ -992,6 +993,104 @@
     }
 
     @Test
+    fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApi() {
+        runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = false)
+    }
+
+    @Test
+    fun testSubtypeAdvertisingAndDiscovery_withSetSubtypesApiAndLegacySpecifier() {
+        runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier = true)
+    }
+
+    private fun runSubtypeAdvertisingAndDiscoveryTest(useLegacySpecifier: Boolean) {
+        val si = makeTestServiceInfo(network = testNetwork1.network)
+        if (useLegacySpecifier) {
+            si.subtypes = setOf("_subtype1")
+
+            // Test "_type._tcp.local,_subtype" syntax with the registration
+            si.serviceType = si.serviceType + ",_subtype2"
+        } else {
+            si.subtypes = setOf("_subtype1", "_subtype2")
+        }
+
+        val registrationRecord = NsdRegistrationRecord()
+
+        val baseTypeDiscoveryRecord = NsdDiscoveryRecord()
+        val subtype1DiscoveryRecord = NsdDiscoveryRecord()
+        val subtype2DiscoveryRecord = NsdDiscoveryRecord()
+        val otherSubtypeDiscoveryRecord = NsdDiscoveryRecord()
+        tryTest {
+            registerService(registrationRecord, si)
+
+            nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, baseTypeDiscoveryRecord)
+
+            // Test "<subtype>._type._tcp.local" syntax with discovery. Note this is not
+            // "<subtype>._sub._type._tcp.local".
+            nsdManager.discoverServices("_othersubtype.$serviceType",
+                    NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, otherSubtypeDiscoveryRecord)
+            nsdManager.discoverServices("_subtype1.$serviceType",
+                    NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, subtype1DiscoveryRecord)
+            nsdManager.discoverServices("_subtype2.$serviceType",
+                    NsdManager.PROTOCOL_DNS_SD,
+                    testNetwork1.network, Executor { it.run() }, subtype2DiscoveryRecord)
+
+            val info1 = subtype1DiscoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            assertTrue(info1.subtypes.contains("_subtype1"))
+            val info2 = subtype2DiscoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            assertTrue(info2.subtypes.contains("_subtype2"))
+            baseTypeDiscoveryRecord.waitForServiceDiscovered(
+                    serviceName, serviceType, testNetwork1.network)
+            otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStarted>()
+            // The subtype callback was registered later but called, no need for an extra delay
+            otherSubtypeDiscoveryRecord.assertNoCallback(timeoutMs = 0)
+        } cleanupStep {
+            nsdManager.stopServiceDiscovery(baseTypeDiscoveryRecord)
+            nsdManager.stopServiceDiscovery(subtype1DiscoveryRecord)
+            nsdManager.stopServiceDiscovery(subtype2DiscoveryRecord)
+            nsdManager.stopServiceDiscovery(otherSubtypeDiscoveryRecord)
+
+            baseTypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+            subtype1DiscoveryRecord.expectCallback<DiscoveryStopped>()
+            subtype2DiscoveryRecord.expectCallback<DiscoveryStopped>()
+            otherSubtypeDiscoveryRecord.expectCallback<DiscoveryStopped>()
+        } cleanup {
+            nsdManager.unregisterService(registrationRecord)
+        }
+    }
+
+    @Test
+    fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
+        val si = makeTestServiceInfo(network = testNetwork1.network)
+        // Sets 101 subtypes in total
+        val seq = generateSequence(1) { it + 1}
+        si.subtypes = seq.take(100).toList().map {it -> "_subtype" + it}.toSet()
+        si.serviceType = si.serviceType + ",_subtype"
+
+        val record = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+        val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+        assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+    }
+
+    @Test
+    fun testSubtypeAdvertising_emptySubtypeLabel_returnsFailureBadParameters() {
+        val si = makeTestServiceInfo(network = testNetwork1.network)
+        si.subtypes = setOf("")
+
+        val record = NsdRegistrationRecord()
+        nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, Executor { it.run() }, record)
+
+        val failedCb = record.expectCallback<RegistrationFailed>(REGISTRATION_TIMEOUT_MS)
+        assertEquals(NsdManager.FAILURE_BAD_PARAMETERS, failedCb.errorCode)
+    }
+
+    @Test
     fun testRegisterWithConflictDuringProbing() {
         // This test requires shims supporting T+ APIs (NsdServiceInfo.network)
         assumeTrue(TestUtils.shouldTestTApis())
diff --git a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
index 1618a62..8037542 100644
--- a/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
+++ b/tests/unit/java/com/android/server/IpSecServiceParameterizedTest.java
@@ -33,6 +33,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
@@ -74,6 +75,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.net.module.util.netlink.xfrm.XfrmNetlinkNewSaMessage;
 import com.android.server.IpSecService.TunnelInterfaceRecord;
 import com.android.testutils.DevSdkIgnoreRule;
 
@@ -85,6 +87,7 @@
 import org.junit.runners.Parameterized;
 
 import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.net.Socket;
 import java.util.Arrays;
 import java.util.Collection;
@@ -149,6 +152,7 @@
         private Set<String> mAllowedPermissions = new ArraySet<>(Arrays.asList(
                 android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
                 android.Manifest.permission.NETWORK_STACK,
+                android.Manifest.permission.ACCESS_NETWORK_STATE,
                 PERMISSION_MAINLINE_NETWORK_STACK));
 
         private void setAllowedPermissions(String... permissions) {
@@ -202,11 +206,13 @@
     private IpSecService.Dependencies makeDependencies() throws RemoteException {
         final IpSecService.Dependencies deps = mock(IpSecService.Dependencies.class);
         when(deps.getNetdInstance(mTestContext)).thenReturn(mMockNetd);
+        when(deps.getIpSecXfrmController()).thenReturn(mMockXfrmCtrl);
         return deps;
     }
 
     INetd mMockNetd;
     PackageManager mMockPkgMgr;
+    IpSecXfrmController mMockXfrmCtrl;
     IpSecService.Dependencies mDeps;
     IpSecService mIpSecService;
     Network fakeNetwork = new Network(0xAB);
@@ -235,6 +241,7 @@
     @Before
     public void setUp() throws Exception {
         mMockNetd = mock(INetd.class);
+        mMockXfrmCtrl = mock(IpSecXfrmController.class);
         mMockPkgMgr = mock(PackageManager.class);
         mDeps = makeDependencies();
         mIpSecService = new IpSecService(mTestContext, mDeps);
@@ -506,6 +513,32 @@
     }
 
     @Test
+    public void getTransformState() throws Exception {
+        XfrmNetlinkNewSaMessage mockXfrmNewSaMsg = mock(XfrmNetlinkNewSaMessage.class);
+        when(mockXfrmNewSaMsg.getBitmap()).thenReturn(new byte[512]);
+        when(mMockXfrmCtrl.ipSecGetSa(any(InetAddress.class), anyLong()))
+                .thenReturn(mockXfrmNewSaMsg);
+
+        // Create transform
+        IpSecConfig ipSecConfig = new IpSecConfig();
+        addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
+        addAuthAndCryptToIpSecConfig(ipSecConfig);
+
+        IpSecTransformResponse createTransformResp =
+                mIpSecService.createTransform(ipSecConfig, new Binder(), BLESSED_PACKAGE);
+        assertEquals(IpSecManager.Status.OK, createTransformResp.status);
+
+        // Get transform state
+        mIpSecService.getTransformState(createTransformResp.resourceId);
+
+        // Verifications
+        verify(mMockXfrmCtrl)
+                .ipSecGetSa(
+                        eq(InetAddresses.parseNumericAddress(mDestinationAddr)),
+                        eq(Integer.toUnsignedLong(TEST_SPI)));
+    }
+
+    @Test
     public void testReleaseOwnedSpi() throws Exception {
         IpSecConfig ipSecConfig = new IpSecConfig();
         addDefaultSpisAndRemoteAddrToIpSecConfig(ipSecConfig);
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 32014c2..e4683be 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -140,6 +140,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Queue;
+import java.util.Set;
 
 // TODOs:
 //  - test client can send requests and receive replies
@@ -1117,7 +1118,8 @@
         waitForIdle();
         verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(s ->
                 "Instance".equals(s.getServiceName())
-                        && SERVICE_TYPE.equals(s.getServiceType())), eq("_subtype"), any());
+                        && SERVICE_TYPE.equals(s.getServiceType())
+                        && s.getSubtypes().equals(Set.of("_subtype"))), any());
 
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
         client.discoverServices(typeWithSubtype, PROTOCOL, network, Runnable::run, discListener);
@@ -1223,7 +1225,7 @@
 
         final ArgumentCaptor<Integer> serviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
         verify(mAdvertiser).addOrUpdateService(serviceIdCaptor.capture(),
-                argThat(info -> matches(info, regInfo)), eq(null) /* subtype */, any());
+                argThat(info -> matches(info, regInfo)), any());
 
         client.unregisterService(regListenerWithoutFeature);
         waitForIdle();
@@ -1283,9 +1285,9 @@
 
         // The advertiser is enabled for _type2 but not _type1
         verify(mAdvertiser, never()).addOrUpdateService(anyInt(),
-                argThat(info -> matches(info, service1)), eq(null) /* subtype */, any());
+                argThat(info -> matches(info, service1)), any());
         verify(mAdvertiser).addOrUpdateService(anyInt(), argThat(info -> matches(info, service2)),
-                eq(null) /* subtype */, any());
+                any());
     }
 
     @Test
@@ -1310,7 +1312,7 @@
         verify(mSocketProvider).startMonitoringSockets();
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
         verify(mAdvertiser).addOrUpdateService(idCaptor.capture(), argThat(info ->
-                matches(info, regInfo)), eq(null) /* subtype */, any());
+                matches(info, regInfo)), any());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1358,7 +1360,7 @@
 
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
-        verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any(), any());
+        verify(mAdvertiser, never()).addOrUpdateService(anyInt(), any(), any());
 
         verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
                 argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
@@ -1388,8 +1390,7 @@
         final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
         // Service name is truncated to 63 characters
         verify(mAdvertiser).addOrUpdateService(idCaptor.capture(),
-                argThat(info -> info.getServiceName().equals("a".repeat(63))),
-                eq(null) /* subtype */, any());
+                argThat(info -> info.getServiceName().equals("a".repeat(63))), any());
 
         // Verify onServiceRegistered callback
         final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
@@ -1479,7 +1480,7 @@
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
-        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
 
         // Verify the discovery uses MdnsDiscoveryManager
         final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -1512,7 +1513,7 @@
         client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
         waitForIdle();
         verify(mSocketProvider).startMonitoringSockets();
-        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any(), any());
+        verify(mAdvertiser).addOrUpdateService(anyInt(), any(), any());
 
         final Network wifiNetwork1 = new Network(123);
         final Network wifiNetwork2 = new Network(124);
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 46e9e45..c9cece0 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -2979,7 +2979,7 @@
                         null /* iface */, RTN_UNREACHABLE));
         assertEquals(expectedRoutes, lp.getRoutes());
 
-        verify(mMockNetworkAgent).unregister();
+        verify(mMockNetworkAgent, timeout(TEST_TIMEOUT_MS)).unregister();
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
index f0cb6df..121f844 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsAdvertiserTest.kt
@@ -68,6 +68,7 @@
 private val TEST_SOCKETKEY_2 = SocketKey(1002 /* interfaceIndex */)
 private val TEST_HOSTNAME = arrayOf("Android_test", "local")
 private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
 private val TEST_INTERFACE1 = "test_iface1"
 private val TEST_INTERFACE2 = "test_iface2"
 private val TEST_OFFLOAD_PACKET1 = byteArrayOf(0x01, 0x02, 0x03)
@@ -80,6 +81,13 @@
     network = TEST_NETWORK_1
 }
 
+private val SERVICE_1_SUBTYPE = NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+    subtypes = setOf(TEST_SUBTYPE)
+    port = 12345
+    hostAddresses = listOf(TEST_ADDR)
+    network = TEST_NETWORK_1
+}
+
 private val LONG_SERVICE_1 =
     NsdServiceInfo("a".repeat(48) + "TestServiceName", "_longadvertisertest._tcp").apply {
     port = 12345
@@ -93,6 +101,14 @@
     network = null
 }
 
+private val ALL_NETWORKS_SERVICE_SUBTYPE =
+        NsdServiceInfo("TestServiceName", "_advertisertest._tcp").apply {
+    subtypes = setOf(TEST_SUBTYPE)
+    port = 12345
+    hostAddresses = listOf(TEST_ADDR)
+    network = null
+}
+
 private val ALL_NETWORKS_SERVICE_2 =
     NsdServiceInfo("TESTSERVICENAME", "_ADVERTISERTEST._tcp").apply {
         port = 12345
@@ -189,7 +205,7 @@
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), socketCbCaptor.capture())
@@ -247,14 +263,14 @@
     }
 
     @Test
-    fun testAddService_AllNetworks() {
+    fun testAddService_AllNetworksWithSubType() {
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
-                TEST_SUBTYPE, DEFAULT_ADVERTISING_OPTION) }
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
+                DEFAULT_ADVERTISING_OPTION) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
-        verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE.network),
+        verify(socketProvider).requestSocket(eq(ALL_NETWORKS_SERVICE_SUBTYPE.network),
                 socketCbCaptor.capture())
 
         val socketCb = socketCbCaptor.value
@@ -270,9 +286,9 @@
                 eq(thread.looper), any(), intAdvCbCaptor2.capture(), eq(TEST_HOSTNAME), any(), any()
         )
         verify(mockInterfaceAdvertiser1).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
         verify(mockInterfaceAdvertiser2).addService(
-                anyInt(), eq(ALL_NETWORKS_SERVICE), eq(TEST_SUBTYPE))
+                anyInt(), eq(ALL_NETWORKS_SERVICE_SUBTYPE))
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor1.value.onServiceProbingSucceeded(
@@ -286,7 +302,7 @@
                 mockInterfaceAdvertiser2, SERVICE_ID_1) }
         verify(cb).onOffloadStartOrUpdate(eq(TEST_INTERFACE2), eq(OFFLOAD_SERVICEINFO))
         verify(cb).onRegisterServiceSucceeded(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE) })
+                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
 
         // Services are conflicted.
         postSync { intAdvCbCaptor1.value.onServiceConflict(mockInterfaceAdvertiser1, SERVICE_ID_1) }
@@ -323,7 +339,7 @@
         val advertiser =
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION) }
 
         val oneNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(TEST_NETWORK_1), oneNetSocketCbCaptor.capture())
@@ -331,18 +347,18 @@
 
         // Register a service with the same name on all networks (name conflict)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION) }
         val allNetSocketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), allNetSocketCbCaptor.capture())
         val allNetSocketCb = allNetSocketCbCaptor.value
 
         postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_1, LONG_SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION) }
         postSync { advertiser.addOrUpdateService(LONG_SERVICE_ID_2, LONG_ALL_NETWORKS_SERVICE,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION) }
 
         postSync { advertiser.addOrUpdateService(CASE_INSENSITIVE_TEST_SERVICE_ID,
-                ALL_NETWORKS_SERVICE_2, null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                ALL_NETWORKS_SERVICE_2, DEFAULT_ADVERTISING_OPTION) }
 
         // Callbacks for matching network and all networks both get the socket
         postSync {
@@ -378,15 +394,15 @@
                 eq(thread.looper), any(), intAdvCbCaptor.capture(), eq(TEST_HOSTNAME), any(), any()
         )
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(SERVICE_1) }, eq(null))
+                argThat { it.matches(SERVICE_1) })
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_2),
-                argThat { it.matches(expectedRenamed) }, eq(null))
+                argThat { it.matches(expectedRenamed) })
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_1),
-                argThat { it.matches(LONG_SERVICE_1) }, eq(null))
+                argThat { it.matches(LONG_SERVICE_1) })
         verify(mockInterfaceAdvertiser1).addService(eq(LONG_SERVICE_ID_2),
-            argThat { it.matches(expectedLongRenamed) }, eq(null))
+            argThat { it.matches(expectedLongRenamed) })
         verify(mockInterfaceAdvertiser1).addService(eq(CASE_INSENSITIVE_TEST_SERVICE_ID),
-            argThat { it.matches(expectedCaseInsensitiveRenamed) }, eq(null))
+            argThat { it.matches(expectedCaseInsensitiveRenamed) })
 
         doReturn(false).`when`(mockInterfaceAdvertiser1).isProbing(SERVICE_ID_1)
         postSync { intAdvCbCaptor.value.onServiceProbingSucceeded(
@@ -411,7 +427,7 @@
         val advertiser =
                 MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION) }
 
         val socketCbCaptor = ArgumentCaptor.forClass(SocketCallback::class.java)
         verify(socketProvider).requestSocket(eq(null), socketCbCaptor.capture())
@@ -420,29 +436,28 @@
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_1, mockSocket1, listOf(TEST_LINKADDR)) }
 
         verify(mockInterfaceAdvertiser1).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(null))
+                argThat { it.matches(ALL_NETWORKS_SERVICE) })
 
         val updateOptions = MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(true).build()
 
         // Update with serviceId that is not registered yet should fail
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_2, ALL_NETWORKS_SERVICE_SUBTYPE,
                 updateOptions) }
         verify(cb).onRegisterServiceFailed(SERVICE_ID_2, NsdManager.FAILURE_INTERNAL_ERROR)
 
         // Update service with different NsdServiceInfo should fail
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1, TEST_SUBTYPE,
-                updateOptions) }
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1_SUBTYPE, updateOptions) }
         verify(cb).onRegisterServiceFailed(SERVICE_ID_1, NsdManager.FAILURE_INTERNAL_ERROR)
 
         // Update service with same NsdServiceInfo but different subType should succeed
-        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE, TEST_SUBTYPE,
+        postSync { advertiser.addOrUpdateService(SERVICE_ID_1, ALL_NETWORKS_SERVICE_SUBTYPE,
                 updateOptions) }
-        verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(TEST_SUBTYPE))
+        verify(mockInterfaceAdvertiser1).updateService(eq(SERVICE_ID_1), eq(setOf(TEST_SUBTYPE)))
 
         // Newly created MdnsInterfaceAdvertiser will get addService() call.
         postSync { socketCb.onSocketCreated(TEST_SOCKETKEY_2, mockSocket2, listOf(TEST_LINKADDR2)) }
         verify(mockInterfaceAdvertiser2).addService(eq(SERVICE_ID_1),
-                argThat { it.matches(ALL_NETWORKS_SERVICE) }, eq(TEST_SUBTYPE))
+                argThat { it.matches(ALL_NETWORKS_SERVICE_SUBTYPE) })
     }
 
     @Test
@@ -451,7 +466,7 @@
             MdnsAdvertiser(thread.looper, socketProvider, cb, mockDeps, sharedlog, flags)
         verify(mockDeps, times(1)).generateHostname()
         postSync { advertiser.addOrUpdateService(SERVICE_ID_1, SERVICE_1,
-                null /* subtype */, DEFAULT_ADVERTISING_OPTION) }
+                DEFAULT_ADVERTISING_OPTION) }
         postSync { advertiser.removeService(SERVICE_ID_1) }
         verify(mockDeps, times(2)).generateHostname()
     }
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index f85d71d..0c04bff 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -67,6 +67,13 @@
     port = 12345
 }
 
+private val TEST_SERVICE_1_SUBTYPE = NsdServiceInfo().apply {
+    subtypes = setOf("_sub")
+    serviceType = "_testservice._tcp"
+    serviceName = "MyTestService"
+    port = 12345
+}
+
 @RunWith(DevSdkIgnoreRunner::class)
 @IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class MdnsInterfaceAdvertiserTest {
@@ -119,7 +126,7 @@
             knownServices.add(inv.getArgument(0))
 
             -1
-        }.`when`(repository).addService(anyInt(), any(), any())
+        }.`when`(repository).addService(anyInt(), any())
         doAnswer { inv ->
             knownServices.remove(inv.getArgument(0))
             null
@@ -277,10 +284,9 @@
     @Test
     fun testReplaceExitingService() {
         doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
-                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
-        val subType = "_sub"
-        advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1, subType)
-        verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+        advertiser.addService(TEST_SERVICE_ID_DUPLICATE, TEST_SERVICE_1_SUBTYPE)
+        verify(repository).addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
         verify(announcer).stop(TEST_SERVICE_ID_DUPLICATE)
         verify(prober).startProbing(any())
     }
@@ -288,9 +294,9 @@
     @Test
     fun testUpdateExistingService() {
         doReturn(TEST_SERVICE_ID_DUPLICATE).`when`(repository)
-                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any(), any())
-        val subType = "_sub"
-        advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subType)
+                .addService(eq(TEST_SERVICE_ID_DUPLICATE), any())
+        val subTypes = setOf("_sub")
+        advertiser.updateService(TEST_SERVICE_ID_DUPLICATE, subTypes)
         verify(repository).updateService(eq(TEST_SERVICE_ID_DUPLICATE), any())
         verify(announcer, never()).stop(TEST_SERVICE_ID_DUPLICATE)
         verify(prober, never()).startProbing(any())
@@ -302,8 +308,8 @@
         doReturn(serviceId).`when`(testProbingInfo).serviceId
         doReturn(testProbingInfo).`when`(repository).setServiceProbing(serviceId)
 
-        advertiser.addService(serviceId, serviceInfo, null /* subtype */)
-        verify(repository).addService(serviceId, serviceInfo, null /* subtype */)
+        advertiser.addService(serviceId, serviceInfo)
+        verify(repository).addService(serviceId, serviceInfo)
         verify(prober).startProbing(testProbingInfo)
 
         // Simulate probing success: continues to announcing
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index c74e330..85e361d 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -27,6 +27,7 @@
 import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
+import com.google.common.truth.Truth.assertThat
 import java.net.InetSocketAddress
 import java.net.NetworkInterface
 import java.util.Collections
@@ -46,6 +47,7 @@
 private const val TEST_SERVICE_ID_3 = 44
 private const val TEST_PORT = 12345
 private const val TEST_SUBTYPE = "_subtype"
+private const val TEST_SUBTYPE2 = "_subtype2"
 private val TEST_HOSTNAME = arrayOf("Android_000102030405060708090A0B0C0D0E0F", "local")
 private val TEST_ADDRESSES = listOf(
         LinkAddress(parseNumericAddress("192.0.2.111"), 24),
@@ -95,8 +97,7 @@
     fun testAddServiceAndProbe() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
         assertEquals(0, repository.servicesCount)
-        assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
-                null /* subtype */))
+        assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -131,10 +132,10 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
         assertFailsWith(NameConflictException::class) {
-            repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */)
+            repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1)
         }
         assertFailsWith(NameConflictException::class) {
-            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3, null /* subtype */)
+            repository.addService(TEST_SERVICE_ID_3, TEST_SERVICE_3)
         }
     }
 
@@ -144,10 +145,10 @@
         repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
 
         assertFailsWith(IllegalArgumentException::class) {
-            repository.updateService(TEST_SERVICE_ID_2, null /* subtype */)
+            repository.updateService(TEST_SERVICE_ID_2, emptySet() /* subtype */)
         }
 
-        repository.updateService(TEST_SERVICE_ID_1, TEST_SUBTYPE)
+        repository.updateService(TEST_SERVICE_ID_1, setOf(TEST_SUBTYPE))
 
         val queriedName = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
         val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
@@ -175,9 +176,9 @@
     @Test
     fun testInvalidReuseOfServiceId() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
         assertFailsWith(IllegalArgumentException::class) {
-            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2, null /* subtype */)
+            repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_2)
         }
     }
 
@@ -186,7 +187,7 @@
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
         assertFalse(repository.hasActiveService(TEST_SERVICE_ID_1))
 
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
         assertTrue(repository.hasActiveService(TEST_SERVICE_ID_1))
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -229,9 +230,10 @@
     }
 
     @Test
-    fun testExitAnnouncements_WithSubtype() {
+    fun testExitAnnouncements_WithSubtypes() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, TEST_SUBTYPE)
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
         repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
 
         val exitAnnouncement = repository.exitService(TEST_SERVICE_ID_1)
@@ -245,7 +247,7 @@
         assertEquals(0, packet.authorityRecords.size)
         assertEquals(0, packet.additionalRecords.size)
 
-        assertContentEquals(listOf(
+        assertThat(packet.answers).containsExactly(
                 MdnsPointerRecord(
                         arrayOf("_testservice", "_tcp", "local"),
                         0L /* receiptTimeMillis */,
@@ -258,7 +260,12 @@
                         false /* cacheFlush */,
                         0L /* ttlMillis */,
                         arrayOf("MyTestService", "_testservice", "_tcp", "local")),
-        ), packet.answers)
+                MdnsPointerRecord(
+                        arrayOf("_subtype2", "_sub", "_testservice", "_tcp", "local"),
+                        0L /* receiptTimeMillis */,
+                        false /* cacheFlush */,
+                        0L /* ttlMillis */,
+                        arrayOf("MyTestService", "_testservice", "_tcp", "local")))
 
         repository.removeService(TEST_SERVICE_ID_1)
         assertEquals(0, repository.servicesCount)
@@ -272,7 +279,7 @@
         repository.exitService(TEST_SERVICE_ID_1)
 
         assertEquals(TEST_SERVICE_ID_1,
-                repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1, null /* subtype */))
+                repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_1))
         assertEquals(1, repository.servicesCount)
 
         repository.removeService(TEST_SERVICE_ID_2)
@@ -283,7 +290,7 @@
     fun testOnProbingSucceeded() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
         val announcementInfo = repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
-                TEST_SUBTYPE)
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
         repository.onAdvertisementSent(TEST_SERVICE_ID_1, 2 /* sentPacketCount */)
         val packet = announcementInfo.getPacket(0)
 
@@ -294,12 +301,13 @@
 
         val serviceType = arrayOf("_testservice", "_tcp", "local")
         val serviceSubtype = arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
+        val serviceSubtype2 = arrayOf(TEST_SUBTYPE2, "_sub", "_testservice", "_tcp", "local")
         val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
         val v4AddrRev = getReverseDnsAddress(TEST_ADDRESSES[0].address)
         val v6Addr1Rev = getReverseDnsAddress(TEST_ADDRESSES[1].address)
         val v6Addr2Rev = getReverseDnsAddress(TEST_ADDRESSES[2].address)
 
-        assertContentEquals(listOf(
+        assertThat(packet.answers).containsExactly(
                 // Reverse address and address records for the hostname
                 MdnsPointerRecord(v4AddrRev,
                         0L /* receiptTimeMillis */,
@@ -346,6 +354,13 @@
                         false /* cacheFlush */,
                         4500000L /* ttlMillis */,
                         serviceName),
+                MdnsPointerRecord(
+                        serviceSubtype2,
+                        0L /* receiptTimeMillis */,
+                        // Not a unique name owned by the announcer, so cacheFlush=false
+                        false /* cacheFlush */,
+                        4500000L /* ttlMillis */,
+                        serviceName),
                 MdnsServiceRecord(
                         serviceName,
                         0L /* receiptTimeMillis */,
@@ -367,8 +382,7 @@
                         0L /* receiptTimeMillis */,
                         false /* cacheFlush */,
                         4500000L /* ttlMillis */,
-                        serviceType)
-        ), packet.answers)
+                        serviceType))
 
         assertContentEquals(listOf(
                 MdnsNsecRecord(v4AddrRev,
@@ -484,19 +498,20 @@
 
     @Test
     fun testGetReply() {
-        doGetReplyTest(subtype = null)
+        doGetReplyTest(queryWithSubtype = false)
     }
 
     @Test
     fun testGetReply_WithSubtype() {
-        doGetReplyTest(TEST_SUBTYPE)
+        doGetReplyTest(queryWithSubtype = true)
     }
 
-    private fun doGetReplyTest(subtype: String?) {
+    private fun doGetReplyTest(queryWithSubtype: Boolean) {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, subtype)
-        val queriedName = if (subtype == null) arrayOf("_testservice", "_tcp", "local")
-        else arrayOf(subtype, "_sub", "_testservice", "_tcp", "local")
+        repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
+                setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
+        val queriedName = if (!queryWithSubtype) arrayOf("_testservice", "_tcp", "local")
+                else arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
 
         val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
         val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
@@ -577,8 +592,8 @@
     @Test
     fun testGetConflictingServices() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val packet = MdnsPacket(
                 0 /* flags */,
@@ -605,8 +620,8 @@
     @Test
     fun testGetConflictingServicesCaseInsensitive() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val packet = MdnsPacket(
             0 /* flags */,
@@ -633,8 +648,8 @@
     @Test
     fun testGetConflictingServices_IdenticalService() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val otherTtlMillis = 1234L
         val packet = MdnsPacket(
@@ -662,8 +677,8 @@
     @Test
     fun testGetConflictingServicesCaseInsensitive_IdenticalService() {
         val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
-        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1, null /* subtype */)
-        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2, null /* subtype */)
+        repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+        repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
 
         val otherTtlMillis = 1234L
         val packet = MdnsPacket(
@@ -718,8 +733,7 @@
                 MdnsFeatureFlags.newBuilder().setIncludeInetAddressRecordsInProbing(true).build())
         repository.updateAddresses(TEST_ADDRESSES)
         assertEquals(0, repository.servicesCount)
-        assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
-                null /* subtype */))
+        assertEquals(-1, repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1))
         assertEquals(1, repository.servicesCount)
 
         val probingInfo = repository.setServiceProbing(TEST_SERVICE_ID_1)
@@ -778,10 +792,11 @@
 private fun MdnsRecordRepository.initWithService(
     serviceId: Int,
     serviceInfo: NsdServiceInfo,
-    subtype: String? = null,
+    subtypes: Set<String> = setOf(),
 ): AnnouncementInfo {
     updateAddresses(TEST_ADDRESSES)
-    addService(serviceId, serviceInfo, subtype)
+    serviceInfo.setSubtypes(subtypes)
+    addService(serviceId, serviceInfo)
     val probingInfo = setServiceProbing(serviceId)
     assertNotNull(probingInfo)
     return onProbingSucceeded(probingInfo)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
index ad21bf5..dd0706b 100644
--- a/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/CSLocalAgentTests.kt
@@ -154,6 +154,7 @@
         val newLnc = LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
                         .addTransportType(TRANSPORT_WIFI)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .build())
                 .build()
         localAgent.sendLocalNetworkConfig(newLnc)
@@ -196,6 +197,7 @@
                 lp = lp("local0"),
                 lnc = FromS(LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .addTransportType(TRANSPORT_WIFI)
                         .build())
                 .build()),
@@ -250,6 +252,7 @@
                 lnc = FromS(LocalNetworkConfig.Builder()
                         .setUpstreamSelector(NetworkRequest.Builder()
                                 .addTransportType(TRANSPORT_WIFI)
+                                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                                 .build())
                         .build()),
                 score = FromS(NetworkScore.Builder()
@@ -296,6 +299,7 @@
         val lnc = FromS(LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
                         .addTransportType(TRANSPORT_WIFI)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .build())
                 .build())
         val localScore = FromS(NetworkScore.Builder().build())
@@ -348,6 +352,7 @@
                 lp = lp("local0"),
                 lnc = FromS(LocalNetworkConfig.Builder()
                         .setUpstreamSelector(NetworkRequest.Builder()
+                                .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                                 .addTransportType(TRANSPORT_WIFI)
                                 .build())
                         .build()),
@@ -377,6 +382,7 @@
         val lnc = FromS(LocalNetworkConfig.Builder()
                 .setUpstreamSelector(NetworkRequest.Builder()
                         .addCapability(NET_CAPABILITY_DUN)
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .build())
                 .build())
         val localAgent = Agent(nc = nc(TRANSPORT_WIFI, NET_CAPABILITY_LOCAL_NETWORK),
@@ -504,6 +510,7 @@
         val lnc = FromS(LocalNetworkConfig.Builder().apply {
             if (haveUpstream) {
                 setUpstreamSelector(NetworkRequest.Builder()
+                        .addForbiddenCapability(NET_CAPABILITY_LOCAL_NETWORK)
                         .addTransportType(TRANSPORT_WIFI)
                         .build())
             }