Merge changes I62771a8f,I5edf0ffb

* changes:
  PacketBuilderTest: add IPv6 TCP test
  PacketBuilder: add IPv6 support
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
index 0ee862a..f7019a5 100644
--- a/staticlibs/device/com/android/net/module/util/BpfMap.java
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -20,6 +20,7 @@
 
 import android.os.ParcelFileDescriptor;
 import android.system.ErrnoException;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -32,6 +33,7 @@
 import java.nio.ByteOrder;
 import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries.
@@ -65,6 +67,27 @@
     private final int mKeySize;
     private final int mValueSize;
 
+    private static ConcurrentHashMap<Pair<String, Integer>, ParcelFileDescriptor> sFdCache =
+            new ConcurrentHashMap<>();
+
+    private static ParcelFileDescriptor cachedBpfFdGet(String path, int mode)
+            throws ErrnoException, NullPointerException {
+        Pair<String, Integer> key = Pair.create(path, mode);
+        // unlocked fetch is safe: map is concurrent read capable, and only inserted into
+        ParcelFileDescriptor fd = sFdCache.get(key);
+        if (fd != null) return fd;
+        // ok, no cached fd present, need to grab a lock
+        synchronized (BpfMap.class) {
+            // need to redo the check
+            fd = sFdCache.get(key);
+            if (fd != null) return fd;
+            // okay, we really haven't opened this before...
+            fd = ParcelFileDescriptor.adoptFd(nativeBpfFdGet(path, mode));
+            sFdCache.put(key, fd);
+            return fd;
+        }
+    }
+
     /**
      * Create a BpfMap map wrapper with "path" of filesystem.
      *
@@ -74,7 +97,7 @@
      */
     public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
             final Class<V> value) throws ErrnoException, NullPointerException {
-        mMapFd = ParcelFileDescriptor.adoptFd(bpfFdGet(path, flag));
+        mMapFd = cachedBpfFdGet(path, flag);
         mKeyClass = key;
         mValueClass = value;
         mKeySize = Struct.getSize(key);
@@ -90,7 +113,7 @@
      */
     @VisibleForTesting
     protected BpfMap(final Class<K> key, final Class<V> value) {
-        mMapFd = ParcelFileDescriptor.adoptFd(-1 /*invalid*/);  // unused
+        mMapFd = null;  // unused
         mKeyClass = key;
         mValueClass = value;
         mKeySize = Struct.getSize(key);
@@ -103,7 +126,7 @@
      */
     @Override
     public void updateEntry(K key, V value) throws ErrnoException {
-        writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+        nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_ANY);
     }
 
     /**
@@ -114,7 +137,8 @@
     public void insertEntry(K key, V value)
             throws ErrnoException, IllegalStateException {
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_NOEXIST);
         } catch (ErrnoException e) {
             if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
 
@@ -130,7 +154,8 @@
     public void replaceEntry(K key, V value)
             throws ErrnoException, NoSuchElementException {
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_EXIST);
         } catch (ErrnoException e) {
             if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
 
@@ -148,13 +173,15 @@
     public boolean insertOrReplaceEntry(K key, V value)
             throws ErrnoException {
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_NOEXIST);
             return true;   /* insert succeeded */
         } catch (ErrnoException e) {
             if (e.errno != EEXIST) throw e;
         }
         try {
-            writeToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+            nativeWriteToMapEntry(mMapFd.getFd(), key.writeToBytes(), value.writeToBytes(),
+                    BPF_EXIST);
             return false;   /* replace succeeded */
         } catch (ErrnoException e) {
             if (e.errno != ENOENT) throw e;
@@ -171,7 +198,7 @@
     /** Remove existing key from eBpf map. Return false if map was not modified. */
     @Override
     public boolean deleteEntry(K key) throws ErrnoException {
-        return deleteMapEntry(mMapFd.getFd(), key.writeToBytes());
+        return nativeDeleteMapEntry(mMapFd.getFd(), key.writeToBytes());
     }
 
     /** Returns {@code true} if this map contains no elements. */
@@ -204,7 +231,7 @@
 
     private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException {
         byte[] nextKey = new byte[mKeySize];
-        if (getNextMapKey(mMapFd.getFd(), key, nextKey)) return nextKey;
+        if (nativeGetNextMapKey(mMapFd.getFd(), key, nextKey)) return nextKey;
 
         return null;
     }
@@ -239,7 +266,7 @@
 
     private byte[] getRawValue(final byte[] key) throws ErrnoException {
         byte[] value = new byte[mValueSize];
-        if (findMapEntry(mMapFd.getFd(), key, value)) return value;
+        if (nativeFindMapEntry(mMapFd.getFd(), key, value)) return value;
 
         return null;
     }
@@ -263,9 +290,13 @@
         }
     }
 
+    /* Empty implementation to implement AutoCloseable, so we can use BpfMaps
+     * with try with resources, but due to persistent FD cache, there is no actual
+     * need to close anything.  File descriptors will actually be closed when we
+     * unlock the BpfMap class and destroy the ParcelFileDescriptor objects.
+     */
     @Override
     public void close() throws IOException {
-        mMapFd.close();
     }
 
     /**
@@ -283,17 +314,25 @@
         }
     }
 
-    private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
+    private static native int nativeBpfFdGet(String path, int mode)
+            throws ErrnoException, NullPointerException;
 
-    private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags)
+    // Note: the following methods appear to not require the object by virtue of taking the
+    // fd as an int argument, but the hidden reference to this is actually what prevents
+    // the object from being garbage collected (and thus potentially maps closed) prior
+    // to the native code actually running (with a possibly already closed fd).
+
+    private native void nativeWriteToMapEntry(int fd, byte[] key, byte[] value, int flags)
             throws ErrnoException;
 
-    private native boolean deleteMapEntry(int fd, byte[] key) throws ErrnoException;
+    private native boolean nativeDeleteMapEntry(int fd, byte[] key) throws ErrnoException;
 
     // If key is found, the operation returns true and the nextKey would reference to the next
     // element.  If key is not found, the operation returns true and the nextKey would reference to
     // the first element.  If key is the last element, false is returned.
-    private native boolean getNextMapKey(int fd, byte[] key, byte[] nextKey) throws ErrnoException;
+    private native boolean nativeGetNextMapKey(int fd, byte[] key, byte[] nextKey)
+            throws ErrnoException;
 
-    private native boolean findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException;
+    private native boolean nativeFindMapEntry(int fd, byte[] key, byte[] value)
+            throws ErrnoException;
 }
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 a216752..9e1e26e 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -51,8 +51,8 @@
             return null;
         }
 
-        int payloadLength = NetlinkConstants.alignedLengthOf(nlmsghdr.nlmsg_len);
-        payloadLength -= StructNlMsgHdr.STRUCT_SIZE;
+        final int messageLength = NetlinkConstants.alignedLengthOf(nlmsghdr.nlmsg_len);
+        final int payloadLength = messageLength - StructNlMsgHdr.STRUCT_SIZE;
         if (payloadLength < 0 || payloadLength > byteBuffer.remaining()) {
             // Malformed message or runt buffer.  Pretend the buffer was consumed.
             byteBuffer.position(byteBuffer.limit());
@@ -68,15 +68,22 @@
         // Netlink family messages. The netlink family is required. Note that the reason for using
         // if-statement is that switch-case can't be used because the OsConstants.NETLINK_* are
         // not constant.
+        final NetlinkMessage parsed;
         if (nlFamily == OsConstants.NETLINK_ROUTE) {
-            return parseRtMessage(nlmsghdr, byteBuffer);
+            parsed = parseRtMessage(nlmsghdr, byteBuffer);
         } else if (nlFamily == OsConstants.NETLINK_INET_DIAG) {
-            return parseInetDiagMessage(nlmsghdr, byteBuffer);
+            parsed = parseInetDiagMessage(nlmsghdr, byteBuffer);
         } else if (nlFamily == OsConstants.NETLINK_NETFILTER) {
-            return parseNfMessage(nlmsghdr, byteBuffer);
+            parsed = parseNfMessage(nlmsghdr, byteBuffer);
+        } else {
+            parsed = null;
         }
 
-        return null;
+        // Advance to the end of the message, regardless of whether the parsing code consumed
+        // all of it or not.
+        byteBuffer.position(startPosition + messageLength);
+
+        return parsed;
     }
 
     @NonNull
diff --git a/staticlibs/framework/com/android/net/module/util/PerUidCounter.java b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
new file mode 100644
index 0000000..0b2de7a
--- /dev/null
+++ b/staticlibs/framework/com/android/net/module/util/PerUidCounter.java
@@ -0,0 +1,103 @@
+/*
+ * 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.net.module.util;
+
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Keeps track of the counters under different uid, fire exception if the counter
+ * exceeded the specified maximum value.
+ *
+ * @hide
+ */
+public class PerUidCounter {
+    private final int mMaxCountPerUid;
+
+    // Map from UID to count that UID has filed.
+    @VisibleForTesting
+    @GuardedBy("mUidToCount")
+    final SparseIntArray mUidToCount = new SparseIntArray();
+
+    /**
+     * Constructor
+     *
+     * @param maxCountPerUid the maximum count per uid allowed
+     */
+    public PerUidCounter(final int maxCountPerUid) {
+        if (maxCountPerUid <= 0) {
+            throw new IllegalArgumentException("Maximum counter value must be positive");
+        }
+        mMaxCountPerUid = maxCountPerUid;
+    }
+
+    /**
+     * Increments the count of the given uid.  Throws an exception if the number
+     * of the counter for the uid exceeds the value of maxCounterPerUid which is the value
+     * passed into the constructor. see: {@link #PerUidCounter(int)}.
+     *
+     * @throws IllegalStateException if the number of counter for the uid exceed
+     *         the allowed number.
+     *
+     * @param uid the uid that the counter was made under
+     */
+    public void incrementCountOrThrow(final int uid) {
+        incrementCountOrThrow(uid, 1 /* numToIncrement */);
+    }
+
+    public synchronized void incrementCountOrThrow(final int uid, final int numToIncrement) {
+        if (numToIncrement <= 0) {
+            throw new IllegalArgumentException("Increment count must be positive");
+        }
+        final long newCount = ((long) mUidToCount.get(uid, 0)) + numToIncrement;
+        if (newCount > mMaxCountPerUid) {
+            throw new IllegalStateException("Uid " + uid + " exceeded its allowed limit");
+        }
+        // Since the count cannot be greater than Integer.MAX_VALUE here since mMaxCountPerUid
+        // is an integer, it is safe to cast to int.
+        mUidToCount.put(uid, (int) newCount);
+    }
+
+    /**
+     * Decrements the count of the given uid. Throws an exception if the number
+     * of the counter goes below zero.
+     *
+     * @throws IllegalStateException if the number of counter for the uid goes below
+     *         zero.
+     *
+     * @param uid the uid that the count was made under
+     */
+    public void decrementCountOrThrow(final int uid) {
+        decrementCountOrThrow(uid, 1 /* numToDecrement */);
+    }
+
+    public synchronized void decrementCountOrThrow(final int uid, final int numToDecrement) {
+        if (numToDecrement <= 0) {
+            throw new IllegalArgumentException("Decrement count must be positive");
+        }
+        final int newCount = mUidToCount.get(uid, 0) - numToDecrement;
+        if (newCount < 0) {
+            throw new IllegalStateException("BUG: too small count " + newCount + " for UID " + uid);
+        } else if (newCount == 0) {
+            mUidToCount.delete(uid);
+        } else {
+            mUidToCount.put(uid, newCount);
+        }
+    }
+}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
index 8f1b9a2..7801c3e 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfUtils.h
@@ -92,7 +92,7 @@
 
 #define KVER(a, b, c) (((a) << 24) + ((b) << 16) + (c))
 
-static inline unsigned kernelVersion() {
+static inline unsigned uncachedKernelVersion() {
     struct utsname buf;
     int ret = uname(&buf);
     if (ret) return 0;
@@ -108,6 +108,11 @@
     return KVER(kver_major, kver_minor, kver_sub);
 }
 
+static inline unsigned kernelVersion() {
+    static unsigned kver = uncachedKernelVersion();
+    return kver;
+}
+
 static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor, unsigned sub) {
     return kernelVersion() >= KVER(major, minor, sub);
 }
diff --git a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
index ac9f9bc..4b035b9 100644
--- a/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
+++ b/staticlibs/native/bpf_headers/include/bpf/bpf_helpers.h
@@ -19,6 +19,17 @@
  *                                                                            *
  ******************************************************************************/
 
+// The actual versions of the bpfloader that shipped in various Android releases
+
+// Android P/Q/R: BpfLoader was initially part of netd,
+// this was later split out into a standalone binary, but was unversioned.
+
+// Android S / 12 (api level 31) - added 'tethering' mainline eBPF support
+#define BPFLOADER_S_VERSION 2u
+
+// Android T / 13 Beta 3 (api level 33) - added support for 'netd_shared'
+#define BPFLOADER_T_BETA3_VERSION 13u
+
 /* For mainline module use, you can #define BPFLOADER_{MIN/MAX}_VER
  * before #include "bpf_helpers.h" to change which bpfloaders will
  * process the resulting .o file.
@@ -177,6 +188,8 @@
 static unsigned long long (*bpf_get_current_pid_tgid)(void) = (void*) BPF_FUNC_get_current_pid_tgid;
 static unsigned long long (*bpf_get_current_uid_gid)(void) = (void*) BPF_FUNC_get_current_uid_gid;
 static unsigned long long (*bpf_get_smp_processor_id)(void) = (void*) BPF_FUNC_get_smp_processor_id;
+static long (*bpf_get_stackid)(void* ctx, void* map, uint64_t flags) = (void*) BPF_FUNC_get_stackid;
+static long (*bpf_get_current_comm)(void* buf, uint32_t buf_size) = (void*) BPF_FUNC_get_current_comm;
 
 #define DEFINE_BPF_PROG_KVER_RANGE_OPT(SECTION_NAME, prog_uid, prog_gid, the_prog, min_kv, max_kv, \
                                        opt)                                                        \
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
index e3f48e5..2e88fc8 100644
--- a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -27,18 +27,18 @@
 
 namespace android {
 
-static jint com_android_net_module_util_BpfMap_bpfFdGet(JNIEnv *env, jobject clazz,
+static jint com_android_net_module_util_BpfMap_nativeBpfFdGet(JNIEnv *env, jclass clazz,
         jstring path, jint mode) {
     ScopedUtfChars pathname(env, path);
 
     jint fd = bpf::bpfFdGet(pathname.c_str(), static_cast<unsigned>(mode));
 
-    if (fd < 0) jniThrowErrnoException(env, "bpfFdGet", errno);
+    if (fd < 0) jniThrowErrnoException(env, "nativeBpfFdGet", errno);
 
     return fd;
 }
 
-static void com_android_net_module_util_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz,
+static void com_android_net_module_util_BpfMap_nativeWriteToMapEntry(JNIEnv *env, jobject self,
         jint fd, jbyteArray key, jbyteArray value, jint flags) {
     ScopedByteArrayRO keyRO(env, key);
     ScopedByteArrayRO valueRO(env, value);
@@ -46,7 +46,7 @@
     int ret = bpf::writeToMapEntry(static_cast<int>(fd), keyRO.get(), valueRO.get(),
             static_cast<int>(flags));
 
-    if (ret) jniThrowErrnoException(env, "writeToMapEntry", errno);
+    if (ret) jniThrowErrnoException(env, "nativeWriteToMapEntry", errno);
 }
 
 static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) {
@@ -56,7 +56,7 @@
     return false;
 }
 
-static jboolean com_android_net_module_util_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz,
+static jboolean com_android_net_module_util_BpfMap_nativeDeleteMapEntry(JNIEnv *env, jobject self,
         jint fd, jbyteArray key) {
     ScopedByteArrayRO keyRO(env, key);
 
@@ -64,10 +64,10 @@
     // to ENOENT.
     int ret = bpf::deleteMapEntry(static_cast<int>(fd), keyRO.get());
 
-    return throwIfNotEnoent(env, "deleteMapEntry", ret, errno);
+    return throwIfNotEnoent(env, "nativeDeleteMapEntry", ret, errno);
 }
 
-static jboolean com_android_net_module_util_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz,
+static jboolean com_android_net_module_util_BpfMap_nativeGetNextMapKey(JNIEnv *env, jobject self,
         jint fd, jbyteArray key, jbyteArray nextKey) {
     // If key is found, the operation returns zero and sets the next key pointer to the key of the
     // next element.  If key is not found, the operation returns zero and sets the next key pointer
@@ -83,10 +83,10 @@
         ret = bpf::getNextMapKey(static_cast<int>(fd), keyRO.get(), nextKeyRW.get());
     }
 
-    return throwIfNotEnoent(env, "getNextMapKey", ret, errno);
+    return throwIfNotEnoent(env, "nativeGetNextMapKey", ret, errno);
 }
 
-static jboolean com_android_net_module_util_BpfMap_findMapEntry(JNIEnv *env, jobject clazz,
+static jboolean com_android_net_module_util_BpfMap_nativeFindMapEntry(JNIEnv *env, jobject self,
         jint fd, jbyteArray key, jbyteArray value) {
     ScopedByteArrayRO keyRO(env, key);
     ScopedByteArrayRW valueRW(env, value);
@@ -95,7 +95,7 @@
     // "value".  If no element is found, the operation returns -1 and sets errno to ENOENT.
     int ret = bpf::findMapEntry(static_cast<int>(fd), keyRO.get(), valueRW.get());
 
-    return throwIfNotEnoent(env, "findMapEntry", ret, errno);
+    return throwIfNotEnoent(env, "nativeFindMapEntry", ret, errno);
 }
 
 /*
@@ -103,16 +103,16 @@
  */
 static const JNINativeMethod gMethods[] = {
     /* name, signature, funcPtr */
-    { "bpfFdGet", "(Ljava/lang/String;I)I",
-        (void*) com_android_net_module_util_BpfMap_bpfFdGet },
-    { "writeToMapEntry", "(I[B[BI)V",
-        (void*) com_android_net_module_util_BpfMap_writeToMapEntry },
-    { "deleteMapEntry", "(I[B)Z",
-        (void*) com_android_net_module_util_BpfMap_deleteMapEntry },
-    { "getNextMapKey", "(I[B[B)Z",
-        (void*) com_android_net_module_util_BpfMap_getNextMapKey },
-    { "findMapEntry", "(I[B[B)Z",
-        (void*) com_android_net_module_util_BpfMap_findMapEntry },
+    { "nativeBpfFdGet", "(Ljava/lang/String;I)I",
+        (void*) com_android_net_module_util_BpfMap_nativeBpfFdGet },
+    { "nativeWriteToMapEntry", "(I[B[BI)V",
+        (void*) com_android_net_module_util_BpfMap_nativeWriteToMapEntry },
+    { "nativeDeleteMapEntry", "(I[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeDeleteMapEntry },
+    { "nativeGetNextMapKey", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeGetNextMapKey },
+    { "nativeFindMapEntry", "(I[B[B)Z",
+        (void*) com_android_net_module_util_BpfMap_nativeFindMapEntry },
 
 };
 
diff --git a/staticlibs/native/tcutils/kernelversion.h b/staticlibs/native/tcutils/kernelversion.h
index 3be1ad2..492444a 100644
--- a/staticlibs/native/tcutils/kernelversion.h
+++ b/staticlibs/native/tcutils/kernelversion.h
@@ -32,7 +32,7 @@
 
 namespace android {
 
-static inline unsigned kernelVersion() {
+static inline unsigned uncachedKernelVersion() {
   struct utsname buf;
   int ret = uname(&buf);
   if (ret)
@@ -51,6 +51,11 @@
   return KVER(kver_major, kver_minor, kver_sub);
 }
 
+static unsigned kernelVersion() {
+  static unsigned kver = uncachedKernelVersion();
+  return kver;
+}
+
 static inline bool isAtLeastKernelVersion(unsigned major, unsigned minor,
                                           unsigned sub) {
   return kernelVersion() >= KVER(major, minor, sub);
diff --git a/staticlibs/netd/libnetdutils/InternetAddressesTest.cpp b/staticlibs/netd/libnetdutils/InternetAddressesTest.cpp
index f75fa76..9e37d11 100644
--- a/staticlibs/netd/libnetdutils/InternetAddressesTest.cpp
+++ b/staticlibs/netd/libnetdutils/InternetAddressesTest.cpp
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include <android-base/macros.h>
+#include <fmt/format.h>
 #include <gtest/gtest.h>
 
 #include "netdutils/InternetAddresses.h"
@@ -438,6 +439,192 @@
     }
 }
 
+TEST(IPPrefixTest, containsPrefix) {
+    const struct {
+        const char* prefix;
+        const char* otherPrefix;
+        const bool expected;
+        std::string asParameters() const {
+            return fmt::format("prefix={}, other={}, expect={}", prefix, otherPrefix, expected);
+        }
+    } testExpectations[] = {
+            {"192.0.0.0/8", "192.0.0.0/8", true},
+            {"192.1.0.0/16", "192.1.0.0/16", true},
+            {"192.1.2.0/24", "192.1.2.0/24", true},
+            {"192.1.2.3/32", "192.1.2.3/32", true},
+            {"0.0.0.0/0", "192.0.0.0/8", true},
+            {"0.0.0.0/0", "192.1.0.0/16", true},
+            {"0.0.0.0/0", "192.1.2.0/24", true},
+            {"0.0.0.0/0", "192.1.2.3/32", true},
+            {"192.0.0.0/8", "192.1.0.0/16", true},
+            {"192.0.0.0/8", "192.1.2.0/24", true},
+            {"192.0.0.0/8", "192.1.2.5/32", true},
+            {"192.1.0.0/16", "192.1.2.0/24", true},
+            {"192.1.0.0/16", "192.1.3.6/32", true},
+            {"192.5.6.0/24", "192.5.6.7/32", true},
+            {"192.1.2.3/32", "192.1.2.0/24", false},
+            {"192.1.2.3/32", "192.1.0.0/16", false},
+            {"192.1.2.3/32", "192.0.0.0/8", false},
+            {"192.1.2.3/32", "0.0.0.0/0", false},
+            {"192.1.2.0/24", "192.1.0.0/16", false},
+            {"192.1.2.0/24", "192.0.0.0/8", false},
+            {"192.1.2.0/24", "0.0.0.0/0", false},
+            {"192.9.0.0/16", "192.0.0.0/8", false},
+            {"192.9.0.0/16", "0.0.0.0/0", false},
+            {"192.0.0.0/8", "0.0.0.0/0", false},
+            {"192.0.0.0/8", "191.0.0.0/8", false},
+            {"191.0.0.0/8", "192.0.0.0/8", false},
+            {"192.8.0.0/16", "192.7.0.0/16", false},
+            {"192.7.0.0/16", "192.8.0.0/16", false},
+            {"192.8.6.0/24", "192.7.5.0/24", false},
+            {"192.7.5.0/24", "192.8.6.0/24", false},
+            {"192.8.6.100/32", "192.8.6.200/32", false},
+            {"192.8.6.200/32", "192.8.6.100/32", false},
+            {"192.0.0.0/8", "192.0.0.0/12", true},
+            {"192.0.0.0/12", "192.0.0.0/8", false},
+            {"2001::/16", "2001::/16", true},
+            {"2001:db8::/32", "2001:db8::/32", true},
+            {"2001:db8:cafe::/48", "2001:db8:cafe::/48", true},
+            {"2001:db8:cafe:d00d::/64", "2001:db8:cafe:d00d::/64", true},
+            {"2001:db8:cafe:d00d:fec0::/80", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"2001:db8:cafe:d00d:fec0:de::/96", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"2001:db8:cafe:d00d:fec0:de:ac::/112", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"2001:db8::cafe:0:1/128", "2001:db8::cafe:0:1/128", true},
+            {"2001::/16", "2001:db8::/32", true},
+            {"2001::/16", "2001:db8:cafe::/48", true},
+            {"2001::/16", "2001:db8:cafe:d00d::/64", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"2001::/16", "2001:db8:cafe:d00d:fec0:de:ac:dd/128", true},
+            {"::/0", "2001::/16", true},
+            {"::/0", "2001:db8::/32", true},
+            {"::/0", "2001:db8:cafe::/48", true},
+            {"::/0", "2001:db8:cafe:d00d::/64", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0::/80", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de::/96", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de:ac::/112", true},
+            {"::/0", "2001:db8:cafe:d00d:fec0:de:ac:dd/128", true},
+            {"2001:db8::dd/128", "2001::/16", false},
+            {"2001:db8::dd/128", "2001:db8::/32", false},
+            {"2001:db8::dd/128", "2001:db8:cafe::/48", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d::/64", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0:de::/96", false},
+            {"2001:db8::dd/128", "2001:db8:cafe:d00d:fec0:de:ac::/112", false},
+            {"2001:db7::/32", "2001:db8::/32", false},
+            {"2001:db8::/32", "2001:db7::/32", false},
+            {"2001:db8:caff::/48", "2001:db8:cafe::/48", false},
+            {"2001:db8:cafe::/48", "2001:db8:caff::/48", false},
+            {"2001:db8:cafe:a00d::/64", "2001:db8:cafe:d00d::/64", false},
+            {"2001:db8:cafe:d00d::/64", "2001:db8:cafe:a00d::/64", false},
+            {"2001:db8:cafe:d00d:fec1::/80", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"2001:db8:cafe:d00d:fec0::/80", "2001:db8:cafe:d00d:fec1::/80", false},
+            {"2001:db8:cafe:d00d:fec0:dd::/96", "2001:db8:cafe:d00d:fec0:ae::/96", false},
+            {"2001:db8:cafe:d00d:fec0:ae::/96", "2001:db8:cafe:d00d:fec0:dd::/96", false},
+            {"2001:db8:cafe:d00d:fec0:de:aa::/112", "2001:db8:cafe:d00d:fec0:de:ac::/112", false},
+            {"2001:db8:cafe:d00d:fec0:de:ac::/112", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"2001:db8::cafe:0:123/128", "2001:db8::cafe:0:456/128", false},
+            {"2001:db8::cafe:0:456/128", "2001:db8::cafe:0:123/128", false},
+            {"2001:db8::/32", "2001:db8::/64", true},
+            {"2001:db8::/64", "2001:db8::/32", false},
+            {"::/0", "0.0.0.0/0", false},
+            {"::/0", "1.0.0.0/8", false},
+            {"::/0", "1.2.0.0/16", false},
+            {"::/0", "1.2.3.0/24", false},
+            {"::/0", "1.2.3.4/32", false},
+            {"2001::/16", "1.2.3.4/32", false},
+            {"2001::db8::/32", "1.2.3.4/32", false},
+            {"2001:db8:cafe::/48", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d::/64", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0::/80", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0:ae::/96", "1.2.3.4/32", false},
+            {"2001:db8:cafe:d00d:fec0:de:aa::/112", "1.2.3.4/32", false},
+            {"0.0.0.0/0", "::/0", false},
+            {"0.0.0.0/0", "2001::/16", false},
+            {"0.0.0.0/0", "2001::db8::/32", false},
+            {"0.0.0.0/0", "2001:db8:cafe::/48", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d::/64", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0::/80", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0:ae::/96", false},
+            {"0.0.0.0/0", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.3.4/32", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.3.0/24", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.2.0.0/16", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+            {"1.0.0.0/8", "2001:db8:cafe:d00d:fec0:de:aa::/112", false},
+    };
+
+    for (const auto& expectation : testExpectations) {
+        SCOPED_TRACE(expectation.asParameters());
+        IPPrefix a = IPPrefix::forString(expectation.prefix);
+        IPPrefix b = IPPrefix::forString(expectation.otherPrefix);
+        EXPECT_EQ(expectation.expected, a.contains(b));
+    }
+}
+
+TEST(IPPrefixTest, containsAddress) {
+    const struct {
+        const char* prefix;
+        const char* address;
+        const bool expected;
+        std::string asParameters() const {
+            return fmt::format("prefix={}, address={}, expect={}", prefix, address, expected);
+        }
+    } testExpectations[] = {
+        {"0.0.0.0/0", "255.255.255.255", true},
+        {"0.0.0.0/0", "1.2.3.4", true},
+        {"0.0.0.0/0", "1.2.3.0", true},
+        {"0.0.0.0/0", "1.2.0.0", true},
+        {"0.0.0.0/0", "1.0.0.0", true},
+        {"0.0.0.0/0", "0.0.0.0", true},
+        {"0.0.0.0/0", "2001:4868:4860::8888", false},
+        {"0.0.0.0/0", "::/0", false},
+        {"192.0.2.0/23", "192.0.2.0", true},
+        {"192.0.2.0/23", "192.0.2.43", true},
+        {"192.0.2.0/23", "192.0.3.21", true},
+        {"192.0.2.0/23", "192.0.0.21", false},
+        {"192.0.2.0/23", "8.8.8.8", false},
+        {"192.0.2.0/23", "2001:4868:4860::8888", false},
+        {"192.0.2.0/23", "::/0", false},
+        {"1.2.3.4/32", "1.2.3.4", true},
+        {"1.2.3.4/32", "1.2.3.5", false},
+        {"10.0.0.0/8", "10.2.0.0", true},
+        {"10.0.0.0/8", "10.2.3.5", true},
+        {"10.0.0.0/8", "10.0.0.0", true},
+        {"10.0.0.0/8", "10.255.255.254", true},
+        {"10.0.0.0/8", "11.0.0.0", false},
+        {"::/0", "2001:db8:f000::ace:d00c", true},
+        {"::/0", "2002:db8:f00::ace:d00d", true},
+        {"::/0", "2001:db7:f00::ace:d00e", true},
+        {"::/0", "2001:db8:f01::bad:d00d", true},
+        {"::/0", "::", true},
+        {"::/0", "0.0.0.0", false},
+        {"::/0", "1.2.3.4", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00c", true},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00d", true},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::ace:d00e", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:db8:f00::bad:d00d", false},
+        {"2001:db8:f00::ace:d00d/127", "2001:4868:4860::8888", false},
+        {"2001:db8:f00::ace:d00d/127", "8.8.8.8", false},
+        {"2001:db8:f00::ace:d00d/127", "0.0.0.0", false},
+        {"2001:db8:f00::ace:d00d/128", "2001:db8:f00::ace:d00d", true},
+        {"2001:db8:f00::ace:d00d/128", "2001:db8:f00::ace:d00c", false},
+        {"2001::/16", "2001::", true},
+        {"2001::/16", "2001:db8:f00::ace:d00d", true},
+        {"2001::/16", "2001:db8:f00::bad:d00d", true},
+        {"2001::/16", "2001::abc", true},
+        {"2001::/16", "2001:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true},
+        {"2001::/16", "2000::", false},
+    };
+
+    for (const auto& expectation : testExpectations) {
+        SCOPED_TRACE(expectation.asParameters());
+        IPPrefix a = IPPrefix::forString(expectation.prefix);
+        IPAddress b = IPAddress::forString(expectation.address);
+        EXPECT_EQ(expectation.expected, a.contains(b));
+    }
+}
+
 TEST(IPPrefixTest, GamutOfOperators) {
     const std::vector<OperatorExpectation<IPPrefix>> kExpectations{
             {EQ, IPPrefix(), IPPrefix()},
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
index d5cbe2b..d10cec7 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
@@ -221,6 +221,12 @@
     in_addr addr4() const noexcept { return mData.ip.v4; }
     in6_addr addr6() const noexcept { return mData.ip.v6; }
     constexpr int length() const noexcept { return mData.cidrlen; }
+    bool contains(const IPPrefix& other) {
+        return length() <= other.length() && IPPrefix(other.ip(), length()).ip() == ip();
+    }
+    bool contains(const IPAddress& other) {
+        return IPPrefix(other, length()).ip() == ip();
+    }
 
     bool isUninitialized() const noexcept;
     std::string toString() const noexcept;
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt
new file mode 100644
index 0000000..0f2d52a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/PerUidCounterTest.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.net.module.util
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PerUidCounterTest {
+    private val UID_A = 1000
+    private val UID_B = 1001
+
+    @Test
+    fun testCounterMaximum() {
+        assertFailsWith<IllegalArgumentException> {
+            PerUidCounter(-1)
+        }
+        assertFailsWith<IllegalArgumentException> {
+            PerUidCounter(0)
+        }
+
+        val largeMaxCounter = PerUidCounter(Integer.MAX_VALUE)
+        largeMaxCounter.incrementCountOrThrow(UID_A, Integer.MAX_VALUE)
+        assertFailsWith<IllegalStateException> {
+            largeMaxCounter.incrementCountOrThrow(UID_A)
+        }
+    }
+
+    @Test
+    fun testIncrementCountOrThrow() {
+        val counter = PerUidCounter(3)
+
+        // Verify the increment count cannot be zero.
+        assertFailsWith<IllegalArgumentException> {
+            counter.incrementCountOrThrow(UID_A, 0)
+        }
+
+        // Verify the counters work independently.
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_B, 2)
+        counter.incrementCountOrThrow(UID_B)
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_A)
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_B)
+        }
+
+        // Verify exception can be triggered again.
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.incrementCountOrThrow(UID_A, 3)
+        }
+    }
+
+    @Test
+    fun testDecrementCountOrThrow() {
+        val counter = PerUidCounter(3)
+
+        // Verify the decrement count cannot be zero.
+        assertFailsWith<IllegalArgumentException> {
+            counter.decrementCountOrThrow(UID_A, 0)
+        }
+
+        // Verify the count cannot go below zero.
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A, 5)
+        }
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A, Integer.MAX_VALUE)
+        }
+
+        // Verify the counters work independently.
+        counter.incrementCountOrThrow(UID_A)
+        counter.incrementCountOrThrow(UID_B)
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A, 3)
+        }
+        counter.decrementCountOrThrow(UID_A)
+        assertFailsWith<IllegalStateException> {
+            counter.decrementCountOrThrow(UID_A)
+        }
+    }
+}
\ No newline at end of file
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 392314f..55cfd50 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
@@ -63,15 +63,7 @@
         return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
     }
 
-    @Test
-    public void testParseRtmRouteAddress() {
-        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_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;
-
+    private void assertRtmRouteMessage(final RtNetlinkRouteMessage routeMsg) {
         final StructNlMsgHdr hdr = routeMsg.getHeader();
         assertNotNull(hdr);
         assertEquals(136, hdr.nlmsg_len);
@@ -97,6 +89,18 @@
         assertEquals((Inet6Address) routeMsg.getGateway(), TEST_IPV6_LINK_LOCAL_GATEWAY);
     }
 
+    @Test
+    public void testParseRtmRouteMessage() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_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;
+        assertRtmRouteMessage(routeMsg);
+    }
+
     private static final String RTM_NEWROUTE_PACK_HEX =
             "4C000000180000060000000000000000"             // struct nlmsghr
             + "0A400000FC02000100000000"                   // struct rtmsg
@@ -143,7 +147,7 @@
             + "08000400DF020000";                          // RTA_OIF
 
     @Test
-    public void testParseRtmRouteAddress_IPv4MappedIPv6Gateway() {
+    public void testParseRtmRouteMessage_IPv4MappedIPv6Gateway() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_GATEWAY_HEX);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
         final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
@@ -160,7 +164,7 @@
             + "08000400DF020000";                          // RTA_OIF
 
     @Test
-    public void testParseRtmRouteAddress_IPv4MappedIPv6Destination() {
+    public void testParseRtmRouteMessage_IPv4MappedIPv6Destination() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_IPV4_MAPPED_IPV6_DST_HEX);
         byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
         final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
@@ -169,6 +173,32 @@
         assertNull(msg);
     }
 
+    // An example of the full RTM_NEWADDR message.
+    private static final String RTM_NEWADDR_HEX =
+            "48000000140000000000000000000000"            // struct nlmsghr
+            + "0A4080FD1E000000"                          // struct ifaddrmsg
+            + "14000100FE800000000000002C415CFFFE096665"  // IFA_ADDRESS
+            + "14000600100E0000201C00002A70000045700000"  // IFA_CACHEINFO
+            + "0800080080000000";                         // IFA_FLAGS
+
+    @Test
+    public void testParseMultipleRtmMessagesInOneByteBuffer() {
+        final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX + RTM_NEWADDR_HEX);
+        byteBuffer.order(ByteOrder.LITTLE_ENDIAN);  // For testing.
+
+        // Try to parse the RTM_NEWROUTE message.
+        NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkRouteMessage);
+        final RtNetlinkRouteMessage routeMsg = (RtNetlinkRouteMessage) msg;
+        assertRtmRouteMessage(routeMsg);
+
+        // Try to parse the RTM_NEWADDR message.
+        msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+        assertNotNull(msg);
+        assertTrue(msg instanceof RtNetlinkAddressMessage);
+    }
+
     @Test
     public void testToString() {
         final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_HEX);
diff --git a/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java b/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java
new file mode 100644
index 0000000..f99700a
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/testutils/DeviceInfoUtilsTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.testutils;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public final class DeviceInfoUtilsTest {
+    /**
+     * Verifies that version string compare logic returns expected result for various cases.
+     * Note that only major and minor number are compared.
+     */
+    @Test
+    public void testMajorMinorVersionCompare() {
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8.1", "4.8"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("4.9", "4.8.1"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("5.0", "4.8"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("5", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("5", "5.0"));
+        assertEquals(1, DeviceInfoUtils.compareMajorMinorVersion("5-beta1", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8.0.0", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8-RC1", "4.8"));
+        assertEquals(0, DeviceInfoUtils.compareMajorMinorVersion("4.8", "4.8"));
+        assertEquals(-1, DeviceInfoUtils.compareMajorMinorVersion("3.10", "4.8.0"));
+        assertEquals(-1, DeviceInfoUtils.compareMajorMinorVersion("4.7.10.10", "4.8"));
+    }
+}