Merge "Fix libs/net tests package"
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index fde9d3e..d4c58f6 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -95,15 +95,37 @@
}
java_library {
- name: "net-utils-device-common-netlink",
- // TODO: Ipv6Utils and Struct stuff could be separated out of the netlink library into
- // an individual Struct library, and remove the net-utils-framework-common lib dependency.
- // But there is no need doing this at the moment.
+ name: "net-utils-device-common-bpf",
+ srcs: [
+ "device/com/android/net/module/util/BpfMap.java",
+ "device/com/android/net/module/util/JniUtil.java",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "29",
+ visibility: [
+ "//frameworks/libs/net/common/tests:__subpackages__",
+ "//frameworks/libs/net/common/testutils:__subpackages__",
+ "//packages/modules/Connectivity:__subpackages__",
+ "//packages/modules/NetworkStack:__subpackages__",
+ ],
+ static_libs: [
+ "net-utils-device-common-struct",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+}
+
+java_library {
+ name: "net-utils-device-common-struct",
srcs: [
"device/com/android/net/module/util/HexDump.java",
"device/com/android/net/module/util/Ipv6Utils.java",
"device/com/android/net/module/util/Struct.java",
- "device/com/android/net/module/util/netlink/*.java",
"device/com/android/net/module/util/structs/*.java",
],
sdk_version: "system_current",
@@ -126,6 +148,30 @@
}
java_library {
+ name: "net-utils-device-common-netlink",
+ srcs: [
+ "device/com/android/net/module/util/netlink/*.java",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "29",
+ visibility: [
+ "//frameworks/libs/net/common/testutils:__subpackages__",
+ "//packages/modules/Connectivity:__subpackages__",
+ "//packages/modules/NetworkStack:__subpackages__",
+ ],
+ static_libs: [
+ "net-utils-device-common-struct",
+ ],
+ libs: [
+ "androidx.annotation_annotation",
+ ],
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+}
+
+java_library {
// TODO : this target should probably be folded into net-utils-device-common
name: "net-utils-device-common-ip",
srcs: [
@@ -268,3 +314,30 @@
"//packages/modules/Wifi/service",
],
}
+
+// This file group is deprecated; new users should use net-utils-annotations
+filegroup {
+ name: "net-utils-annotations-srcs",
+ srcs: [
+ "annotations/android/net/annotations/PolicyDirection.java",
+ ],
+ visibility: [
+ "//frameworks/base",
+ ],
+}
+
+
+java_library {
+ name: "net-utils-annotations",
+ srcs: [":net-utils-annotations-srcs"],
+ libs: [
+ "framework-annotations-lib",
+ ],
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ visibility: ["//visibility:public"],
+ apex_available: [
+ "//apex_available:anyapex",
+ "//apex_available:platform",
+ ],
+}
diff --git a/staticlibs/annotations/android/net/annotations/PolicyDirection.java b/staticlibs/annotations/android/net/annotations/PolicyDirection.java
new file mode 100644
index 0000000..febd9b4
--- /dev/null
+++ b/staticlibs/annotations/android/net/annotations/PolicyDirection.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2019 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.annotations;
+
+import android.annotation.IntDef;
+import android.net.IpSecManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * IPsec traffic direction.
+ *
+ * <p>Mainline modules cannot reference hidden @IntDef. Moving this annotation to a separate class
+ * to allow others to statically include it.
+ *
+ * @hide
+ */
+@IntDef(value = {IpSecManager.DIRECTION_IN, IpSecManager.DIRECTION_OUT})
+@Retention(RetentionPolicy.SOURCE)
+public @interface PolicyDirection {}
diff --git a/staticlibs/device/com/android/net/module/util/BpfMap.java b/staticlibs/device/com/android/net/module/util/BpfMap.java
new file mode 100644
index 0000000..5f05c7c
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/BpfMap.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2020 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 static android.system.OsConstants.EEXIST;
+import static android.system.OsConstants.ENOENT;
+
+import android.system.ErrnoException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.net.module.util.Struct;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+/**
+ * BpfMap is a key -> value mapping structure that is designed to maintained the bpf map entries.
+ * This is a wrapper class of in-kernel data structure. The in-kernel data can be read/written by
+ * passing syscalls with map file descriptor.
+ *
+ * @param <K> the key of the map.
+ * @param <V> the value of the map.
+ */
+public class BpfMap<K extends Struct, V extends Struct> implements AutoCloseable {
+ static {
+ System.loadLibrary(JniUtil.getJniLibraryName(BpfMap.class.getPackage()));
+ }
+
+ // Following definitions from kernel include/uapi/linux/bpf.h
+ public static final int BPF_F_RDWR = 0;
+ public static final int BPF_F_RDONLY = 1 << 3;
+ public static final int BPF_F_WRONLY = 1 << 4;
+
+ public static final int BPF_MAP_TYPE_HASH = 1;
+
+ private static final int BPF_F_NO_PREALLOC = 1;
+
+ private static final int BPF_ANY = 0;
+ private static final int BPF_NOEXIST = 1;
+ private static final int BPF_EXIST = 2;
+
+ private final int mMapFd;
+ private final Class<K> mKeyClass;
+ private final Class<V> mValueClass;
+ private final int mKeySize;
+ private final int mValueSize;
+
+ /**
+ * Create a BpfMap map wrapper with "path" of filesystem.
+ *
+ * @param flag the access mode, one of BPF_F_RDWR, BPF_F_RDONLY, or BPF_F_WRONLY.
+ * @throws ErrnoException if the BPF map associated with {@code path} cannot be retrieved.
+ * @throws NullPointerException if {@code path} is null.
+ */
+ public BpfMap(@NonNull final String path, final int flag, final Class<K> key,
+ final Class<V> value) throws ErrnoException, NullPointerException {
+ mMapFd = bpfFdGet(path, flag);
+
+ mKeyClass = key;
+ mValueClass = value;
+ mKeySize = Struct.getSize(key);
+ mValueSize = Struct.getSize(value);
+ }
+
+ /**
+ * Constructor for testing only.
+ * The derived class implements an internal mocked map. It need to implement all functions
+ * which are related with the native BPF map because the BPF map handler is not initialized.
+ * See BpfCoordinatorTest#TestBpfMap.
+ */
+ @VisibleForTesting
+ protected BpfMap(final Class<K> key, final Class<V> value) {
+ mMapFd = -1;
+ mKeyClass = key;
+ mValueClass = value;
+ mKeySize = Struct.getSize(key);
+ mValueSize = Struct.getSize(value);
+ }
+
+ /**
+ * Update an existing or create a new key -> value entry in an eBbpf map.
+ * (use insertOrReplaceEntry() if you need to know whether insert or replace happened)
+ */
+ public void updateEntry(K key, V value) throws ErrnoException {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_ANY);
+ }
+
+ /**
+ * If the key does not exist in the map, insert key -> value entry into eBpf map.
+ * Otherwise IllegalStateException will be thrown.
+ */
+ public void insertEntry(K key, V value)
+ throws ErrnoException, IllegalStateException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+ } catch (ErrnoException e) {
+ if (e.errno == EEXIST) throw new IllegalStateException(key + " already exists");
+
+ throw e;
+ }
+ }
+
+ /**
+ * If the key already exists in the map, replace its value. Otherwise NoSuchElementException
+ * will be thrown.
+ */
+ public void replaceEntry(K key, V value)
+ throws ErrnoException, NoSuchElementException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+ } catch (ErrnoException e) {
+ if (e.errno == ENOENT) throw new NoSuchElementException(key + " not found");
+
+ throw e;
+ }
+ }
+
+ /**
+ * Update an existing or create a new key -> value entry in an eBbpf map.
+ * Returns true if inserted, false if replaced.
+ * (use updateEntry() if you don't care whether insert or replace happened)
+ * Note: see inline comment below if running concurrently with delete operations.
+ */
+ public boolean insertOrReplaceEntry(K key, V value)
+ throws ErrnoException {
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_NOEXIST);
+ return true; /* insert succeeded */
+ } catch (ErrnoException e) {
+ if (e.errno != EEXIST) throw e;
+ }
+ try {
+ writeToMapEntry(mMapFd, key.writeToBytes(), value.writeToBytes(), BPF_EXIST);
+ return false; /* replace succeeded */
+ } catch (ErrnoException e) {
+ if (e.errno != ENOENT) throw e;
+ }
+ /* If we reach here somebody deleted after our insert attempt and before our replace:
+ * this implies a race happened. The kernel bpf delete interface only takes a key,
+ * and not the value, so we can safely pretend the replace actually succeeded and
+ * was immediately followed by the other thread's delete, since the delete cannot
+ * observe the potential change to the value.
+ */
+ return false; /* pretend replace succeeded */
+ }
+
+ /** Remove existing key from eBpf map. Return false if map was not modified. */
+ public boolean deleteEntry(K key) throws ErrnoException {
+ return deleteMapEntry(mMapFd, key.writeToBytes());
+ }
+
+ /** Returns {@code true} if this map contains no elements. */
+ public boolean isEmpty() throws ErrnoException {
+ return getFirstKey() == null;
+ }
+
+ private K getNextKeyInternal(@Nullable K key) throws ErrnoException {
+ final byte[] rawKey = getNextRawKey(
+ key == null ? null : key.writeToBytes());
+ if (rawKey == null) return null;
+
+ final ByteBuffer buffer = ByteBuffer.wrap(rawKey);
+ buffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(mKeyClass, buffer);
+ }
+
+ /**
+ * Get the next key of the passed-in key. If the passed-in key is not found, return the first
+ * key. If the passed-in key is the last one, return null.
+ *
+ * TODO: consider allowing null passed-in key.
+ */
+ public K getNextKey(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+ return getNextKeyInternal(key);
+ }
+
+ private byte[] getNextRawKey(@Nullable final byte[] key) throws ErrnoException {
+ byte[] nextKey = new byte[mKeySize];
+ if (getNextMapKey(mMapFd, key, nextKey)) return nextKey;
+
+ return null;
+ }
+
+ /** Get the first key of eBpf map. */
+ public K getFirstKey() throws ErrnoException {
+ return getNextKeyInternal(null);
+ }
+
+ /** Check whether a key exists in the map. */
+ public boolean containsKey(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+
+ final byte[] rawValue = getRawValue(key.writeToBytes());
+ return rawValue != null;
+ }
+
+ /** Retrieve a value from the map. Return null if there is no such key. */
+ public V getValue(@NonNull K key) throws ErrnoException {
+ Objects.requireNonNull(key);
+ final byte[] rawValue = getRawValue(key.writeToBytes());
+
+ if (rawValue == null) return null;
+
+ final ByteBuffer buffer = ByteBuffer.wrap(rawValue);
+ buffer.order(ByteOrder.nativeOrder());
+ return Struct.parse(mValueClass, buffer);
+ }
+
+ private byte[] getRawValue(final byte[] key) throws ErrnoException {
+ byte[] value = new byte[mValueSize];
+ if (findMapEntry(mMapFd, key, value)) return value;
+
+ return null;
+ }
+
+ /**
+ * Iterate through the map and handle each key -> value retrieved base on the given BiConsumer.
+ * The given BiConsumer may to delete the passed-in entry, but is not allowed to perform any
+ * other structural modifications to the map, such as adding entries or deleting other entries.
+ * Otherwise, iteration will result in undefined behaviour.
+ */
+ public void forEach(BiConsumer<K, V> action) throws ErrnoException {
+ @Nullable K nextKey = getFirstKey();
+
+ while (nextKey != null) {
+ @NonNull final K curKey = nextKey;
+ @NonNull final V value = getValue(curKey);
+
+ nextKey = getNextKey(curKey);
+ action.accept(curKey, value);
+ }
+ }
+
+ @Override
+ public void close() throws ErrnoException {
+ closeMap(mMapFd);
+ }
+
+ /**
+ * Clears the map. The map may already be empty.
+ *
+ * @throws ErrnoException if the map is already closed, if an error occurred during iteration,
+ * or if a non-ENOENT error occurred when deleting a key.
+ */
+ public void clear() throws ErrnoException {
+ K key = getFirstKey();
+ while (key != null) {
+ deleteEntry(key); // ignores ENOENT.
+ key = getFirstKey();
+ }
+ }
+
+ private static native int closeMap(int fd) throws ErrnoException;
+
+ private native int bpfFdGet(String path, int mode) throws ErrnoException, NullPointerException;
+
+ private native void writeToMapEntry(int fd, byte[] key, byte[] value, int flags)
+ throws ErrnoException;
+
+ private native boolean deleteMapEntry(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 findMapEntry(int fd, byte[] key, byte[] value) throws ErrnoException;
+}
diff --git a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
index 77b7835..30a1c33 100644
--- a/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
+++ b/staticlibs/device/com/android/net/module/util/DeviceConfigUtils.java
@@ -35,6 +35,12 @@
private DeviceConfigUtils() {}
private static final String TAG = DeviceConfigUtils.class.getSimpleName();
+ /**
+ * DO NOT MODIFY: this may be used by multiple modules that will not see the updated value
+ * until they are recompiled, so modifying this constant means that different modules may
+ * be referencing a different tethering module variant, or having a stale reference.
+ */
+ public static final String TETHERING_MODULE_NAME = "com.android.tethering";
@VisibleForTesting
public static void resetPackageVersionCacheForTest() {
diff --git a/staticlibs/device/com/android/net/module/util/JniUtil.java b/staticlibs/device/com/android/net/module/util/JniUtil.java
new file mode 100644
index 0000000..5210a3e
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/JniUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * Utilities for modules to use jni.
+ */
+public final class JniUtil {
+ /**
+ * The method to find jni library accroding to the giving package name.
+ *
+ * The jni library name would be packageName + _jni.so. E.g.
+ * com_android_networkstack_tethering_util_jni for tethering,
+ * com_android_connectivity_util_jni for connectivity.
+ */
+ public static String getJniLibraryName(final Package pkg) {
+ final String libPrefix = pkg.getName().replaceAll("\\.", "_");
+
+ return libPrefix + "_jni";
+ }
+}
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 07b52d8..83a82b7 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkConstants.java
@@ -146,12 +146,26 @@
public static final int RTMGRP_LINK = 1;
public static final int RTMGRP_IPV4_IFADDR = 0x10;
public static final int RTMGRP_IPV6_IFADDR = 0x100;
+ public static final int RTMGRP_IPV6_ROUTE = 0x400;
public static final int RTNLGRP_ND_USEROPT = 20;
public static final int RTMGRP_ND_USEROPT = 1 << (RTNLGRP_ND_USEROPT - 1);
// Device flags.
public static final int IFF_LOWER_UP = 1 << 16;
+ // Known values for struct rtmsg rtm_protocol.
+ public static final short RTPROT_KERNEL = 2;
+ public static final short RTPROT_RA = 9;
+
+ // Known values for struct rtmsg rtm_scope.
+ public static final short RT_SCOPE_UNIVERSE = 0;
+
+ // Known values for struct rtmsg rtm_type.
+ public static final short RTN_UNICAST = 1;
+
+ // Known values for struct rtmsg rtm_flags.
+ public static final int RTM_F_CLONED = 0x200;
+
/**
* Convert a netlink message type to a string for control message.
*/
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 708736e..a216752 100644
--- a/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
+++ b/staticlibs/device/com/android/net/module/util/netlink/NetlinkMessage.java
@@ -126,6 +126,9 @@
case NetlinkConstants.RTM_NEWADDR:
case NetlinkConstants.RTM_DELADDR:
return (NetlinkMessage) RtNetlinkAddressMessage.parse(nlmsghdr, byteBuffer);
+ case NetlinkConstants.RTM_NEWROUTE:
+ case NetlinkConstants.RTM_DELROUTE:
+ return (NetlinkMessage) RtNetlinkRouteMessage.parse(nlmsghdr, byteBuffer);
case NetlinkConstants.RTM_NEWNEIGH:
case NetlinkConstants.RTM_DELNEIGH:
case NetlinkConstants.RTM_GETNEIGH:
diff --git a/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
new file mode 100644
index 0000000..c5efcb2
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/RtNetlinkRouteMessage.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 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.netlink;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+
+/**
+ * A NetlinkMessage subclass for rtnetlink route messages.
+ *
+ * RtNetlinkRouteMessage.parse() must be called with a ByteBuffer that contains exactly one
+ * netlink message.
+ *
+ * see also:
+ *
+ * include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class RtNetlinkRouteMessage extends NetlinkMessage {
+ public static final short RTA_DST = 1;
+ public static final short RTA_OIF = 4;
+ public static final short RTA_GATEWAY = 5;
+
+ private int mIfindex;
+ @NonNull
+ private StructRtMsg mRtmsg;
+ @NonNull
+ private IpPrefix mDestination;
+ @Nullable
+ private InetAddress mGateway;
+
+ private RtNetlinkRouteMessage(StructNlMsgHdr header) {
+ super(header);
+ mRtmsg = null;
+ mDestination = null;
+ mGateway = null;
+ mIfindex = 0;
+ }
+
+ public int getInterfaceIndex() {
+ return mIfindex;
+ }
+
+ @NonNull
+ public StructRtMsg getRtMsgHeader() {
+ return mRtmsg;
+ }
+
+ @NonNull
+ public IpPrefix getDestination() {
+ return mDestination;
+ }
+
+ @Nullable
+ public InetAddress getGateway() {
+ return mGateway;
+ }
+
+ /**
+ * Check whether the address families of destination and gateway match rtm_family in
+ * StructRtmsg.
+ *
+ * For example, IPv4-mapped IPv6 addresses as an IPv6 address will be always converted to IPv4
+ * address, that's incorrect when upper layer creates a new {@link RouteInfo} class instance
+ * for IPv6 route with the converted IPv4 gateway.
+ */
+ private static boolean matchRouteAddressFamily(@NonNull final InetAddress address,
+ int family) {
+ return ((address instanceof Inet4Address) && (family == AF_INET))
+ || ((address instanceof Inet6Address) && (family == AF_INET6));
+ }
+
+ /**
+ * Parse rtnetlink route message from {@link ByteBuffer}. This method must be called with a
+ * ByteBuffer that contains exactly one netlink message.
+ *
+ * @param header netlink message header.
+ * @param byteBuffer the ByteBuffer instance that wraps the raw netlink message bytes.
+ */
+ @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;
+ int rtmFamily = routeMsg.mRtmsg.family;
+
+ // RTA_DST
+ final int baseOffset = byteBuffer.position();
+ StructNlAttr nlAttr = StructNlAttr.findNextAttrOfType(RTA_DST, byteBuffer);
+ if (nlAttr != null) {
+ final InetAddress destination = nlAttr.getValueAsInetAddress();
+ // If the RTA_DST attribute is malformed, return null.
+ if (destination == null) return null;
+ // If the address family of destination doesn't match rtm_family, return null.
+ if (!matchRouteAddressFamily(destination, rtmFamily)) return null;
+ 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) {
+ routeMsg.mDestination = new IpPrefix(IPV6_ADDR_ANY, 0);
+ } else {
+ return null;
+ }
+
+ // RTA_GATEWAY
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_GATEWAY, byteBuffer);
+ if (nlAttr != null) {
+ routeMsg.mGateway = nlAttr.getValueAsInetAddress();
+ // If the RTA_GATEWAY attribute is malformed, return null.
+ if (routeMsg.mGateway == null) return null;
+ // If the address family of gateway doesn't match rtm_family, return null.
+ if (!matchRouteAddressFamily(routeMsg.mGateway, rtmFamily)) return null;
+ }
+
+ // RTA_OIF
+ byteBuffer.position(baseOffset);
+ nlAttr = StructNlAttr.findNextAttrOfType(RTA_OIF, byteBuffer);
+ if (nlAttr != null) {
+ // Any callers that deal with interface names are responsible for converting
+ // 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 */);
+ }
+
+ return routeMsg;
+ }
+
+ /**
+ * Write a rtnetlink address message to {@link ByteBuffer}.
+ */
+ @VisibleForTesting
+ protected void pack(ByteBuffer byteBuffer) {
+ getHeader().pack(byteBuffer);
+ mRtmsg.pack(byteBuffer);
+
+ 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);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{" + mHeader.toString(OsConstants.NETLINK_ROUTE) + "}, "
+ + "Rtmsg{" + mRtmsg.toString() + "}, "
+ + "destination{" + mDestination.getAddress().getHostAddress() + "}, "
+ + "gateway{" + (mGateway == null ? "" : mGateway.getHostAddress()) + "}, "
+ + "ifindex{" + mIfindex + "} "
+ + "}";
+ }
+}
diff --git a/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
new file mode 100644
index 0000000..3cd7292
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/netlink/StructRtMsg.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 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.netlink;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.nio.ByteBuffer;
+
+/**
+ * struct rtmsg
+ *
+ * see also:
+ *
+ * include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class StructRtMsg extends Struct {
+ // Already aligned.
+ public static final int STRUCT_SIZE = 12;
+
+ @Field(order = 0, type = Type.U8)
+ public final short family; // Address family of route.
+ @Field(order = 1, type = Type.U8)
+ public final short dstLen; // Length of destination.
+ @Field(order = 2, type = Type.U8)
+ public final short srcLen; // Length of source.
+ @Field(order = 3, type = Type.U8)
+ public final short tos; // TOS filter.
+ @Field(order = 4, type = Type.U8)
+ public final short table; // Routing table ID.
+ @Field(order = 5, type = Type.U8)
+ public final short protocol; // Routing protocol.
+ @Field(order = 6, type = Type.U8)
+ public final short scope; // distance to the destination.
+ @Field(order = 7, type = Type.U8)
+ public final short type; // route type
+ @Field(order = 8, type = Type.U32)
+ public final long flags;
+
+ StructRtMsg(short family, short dstLen, short srcLen, short tos, short table, short protocol,
+ short scope, short type, long flags) {
+ this.family = family;
+ this.dstLen = dstLen;
+ this.srcLen = srcLen;
+ this.tos = tos;
+ this.table = table;
+ this.protocol = protocol;
+ this.scope = scope;
+ this.type = type;
+ this.flags = flags;
+ }
+
+ /**
+ * Parse a rtmsg struct from a {@link ByteBuffer}.
+ *
+ * @param byteBuffer The buffer from which to parse the rtmsg struct.
+ * @return the parsed rtmsg struct, or {@code null} if the rtmsg struct could not be
+ * parsed successfully (for example, if it was truncated).
+ */
+ @Nullable
+ public static StructRtMsg parse(@NonNull final ByteBuffer byteBuffer) {
+ if (byteBuffer.remaining() < STRUCT_SIZE) return null;
+
+ // The ByteOrder must already have been set to native order.
+ return Struct.parse(StructRtMsg.class, byteBuffer);
+ }
+
+ /**
+ * Write the rtmsg struct to {@link ByteBuffer}.
+ */
+ public void pack(@NonNull final ByteBuffer byteBuffer) {
+ // The ByteOrder must already have been set to native order.
+ this.writeToByteBuffer(byteBuffer);
+ }
+}
diff --git a/staticlibs/native/OWNERS b/staticlibs/native/OWNERS
deleted file mode 100644
index 7655338..0000000
--- a/staticlibs/native/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-maze@google.com
diff --git a/staticlibs/native/README.md b/staticlibs/native/README.md
new file mode 100644
index 0000000..18d19c4
--- /dev/null
+++ b/staticlibs/native/README.md
@@ -0,0 +1,27 @@
+# JNI
+As a general rule, jarjar every static library dependency used in a mainline module into the
+modules's namespace (especially if it is also used by other modules)
+
+Fully-qualified name of java class needs to be hard-coded into the JNI .so, because JNI_OnLoad
+does not take any parameters. This means that there needs to be a different .so target for each
+post-jarjared package, so for each module.
+
+This is the guideline to provide JNI library shared with modules:
+
+* provide a common java library in frameworks/libs/net with the Java class (e.g. BpfMap.java).
+
+* provide a common native library in frameworks/libs/net with the JNI and provide the native
+ register function with class_name parameter. See register_com_android_net_module_util_BpfMap
+ function in frameworks/libs/net/common/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
+ as an example.
+
+When you want to use JNI library from frameworks/lib/net:
+
+* Each module includes the java library (e.g. net-utils-device-common-bpf) and applies its jarjar
+ rules after build.
+
+* Each module creates a native library in their directory, which statically links against the
+ common native library (e.g. libnet_utils_device_common_bpf), and calls the native registered
+ function by hardcoding the post-jarjar class_name.
+
+
diff --git a/staticlibs/native/bpf_syscall_wrappers/Android.bp b/staticlibs/native/bpf_syscall_wrappers/Android.bp
index 136342c..1416b6b 100644
--- a/staticlibs/native/bpf_syscall_wrappers/Android.bp
+++ b/staticlibs/native/bpf_syscall_wrappers/Android.bp
@@ -33,6 +33,7 @@
"com.android.tethering",
],
visibility: [
+ "//frameworks/libs/net/common/native/bpfmapjni",
"//packages/modules/Connectivity/service",
"//packages/modules/Connectivity/Tethering",
"//system/bpf/libbpf_android",
diff --git a/staticlibs/native/bpfmapjni/Android.bp b/staticlibs/native/bpfmapjni/Android.bp
new file mode 100644
index 0000000..b7af22d
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/Android.bp
@@ -0,0 +1,44 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+ name: "libnet_utils_device_common_bpfjni",
+ srcs: ["com_android_net_module_util_BpfMap.cpp"],
+ header_libs: [
+ "bpf_syscall_wrappers",
+ "jni_headers",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper_compat_libc++",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ ],
+ sdk_version: "30",
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
+ ],
+}
diff --git a/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
new file mode 100644
index 0000000..e25e17d
--- /dev/null
+++ b/staticlibs/native/bpfmapjni/com_android_net_module_util_BpfMap.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#include <errno.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
+
+#include "nativehelper/scoped_primitive_array.h"
+#include "nativehelper/scoped_utf_chars.h"
+
+#define BPF_FD_JUST_USE_INT
+#include "BpfSyscallWrappers.h"
+
+namespace android {
+
+static jint com_android_net_module_util_BpfMap_closeMap(JNIEnv *env, jobject clazz,
+ jint fd) {
+ int ret = close(fd);
+
+ if (ret) jniThrowErrnoException(env, "closeMap", errno);
+
+ return ret;
+}
+
+static jint com_android_net_module_util_BpfMap_bpfFdGet(JNIEnv *env, jobject 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);
+
+ return fd;
+}
+
+static void com_android_net_module_util_BpfMap_writeToMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray value, jint flags) {
+ ScopedByteArrayRO keyRO(env, key);
+ ScopedByteArrayRO valueRO(env, value);
+
+ int ret = bpf::writeToMapEntry(static_cast<int>(fd), keyRO.get(), valueRO.get(),
+ static_cast<int>(flags));
+
+ if (ret) jniThrowErrnoException(env, "writeToMapEntry", errno);
+}
+
+static jboolean throwIfNotEnoent(JNIEnv *env, const char* functionName, int ret, int err) {
+ if (ret == 0) return true;
+
+ if (err != ENOENT) jniThrowErrnoException(env, functionName, err);
+ return false;
+}
+
+static jboolean com_android_net_module_util_BpfMap_deleteMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key) {
+ ScopedByteArrayRO keyRO(env, key);
+
+ // On success, zero is returned. If the element is not found, -1 is returned and errno is set
+ // to ENOENT.
+ int ret = bpf::deleteMapEntry(static_cast<int>(fd), keyRO.get());
+
+ return throwIfNotEnoent(env, "deleteMapEntry", ret, errno);
+}
+
+static jboolean com_android_net_module_util_BpfMap_getNextMapKey(JNIEnv *env, jobject clazz,
+ 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
+ // to the key of the first element. If key is the last element, -1 is returned and errno is
+ // set to ENOENT. Other possible errno values are ENOMEM, EFAULT, EPERM, and EINVAL.
+ ScopedByteArrayRW nextKeyRW(env, nextKey);
+ int ret;
+ if (key == nullptr) {
+ // Called by getFirstKey. Find the first key in the map.
+ ret = bpf::getNextMapKey(static_cast<int>(fd), nullptr, nextKeyRW.get());
+ } else {
+ ScopedByteArrayRO keyRO(env, key);
+ ret = bpf::getNextMapKey(static_cast<int>(fd), keyRO.get(), nextKeyRW.get());
+ }
+
+ return throwIfNotEnoent(env, "getNextMapKey", ret, errno);
+}
+
+static jboolean com_android_net_module_util_BpfMap_findMapEntry(JNIEnv *env, jobject clazz,
+ jint fd, jbyteArray key, jbyteArray value) {
+ ScopedByteArrayRO keyRO(env, key);
+ ScopedByteArrayRW valueRW(env, value);
+
+ // If an element is found, the operation returns zero and stores the element's value into
+ // "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);
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ { "closeMap", "(I)I",
+ (void*) com_android_net_module_util_BpfMap_closeMap },
+ { "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 },
+
+};
+
+int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name) {
+ return jniRegisterNativeMethods(env,
+ class_name,
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index a9f9d70..07a8200 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -17,6 +17,7 @@
"androidx.test.rules",
"mockito-target-extended-minus-junit4",
"net-utils-device-common",
+ "net-utils-device-common-bpf",
"net-tests-utils",
"netd-client",
],
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt
new file mode 100644
index 0000000..7574087
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/JniUtilTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 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 kotlin.test.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+public final class JniUtilTest {
+ private val TEST_JAVA_UTIL_NAME = "java_util_jni"
+ private val TEST_ORG_JUNIT_NAME = "org_junit_jni"
+
+ @Test
+ fun testGetJniLibraryName() {
+ assertEquals(TEST_JAVA_UTIL_NAME,
+ JniUtil.getJniLibraryName(java.util.Set::class.java.getPackage()))
+ assertEquals(TEST_ORG_JUNIT_NAME,
+ JniUtil.getJniLibraryName(org.junit.Before::class.java.getPackage()))
+ }
+}
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
new file mode 100644
index 0000000..392314f
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/netlink/RtNetlinkRouteMessageTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2021 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.netlink;
+
+import static android.system.OsConstants.NETLINK_ROUTE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.net.InetAddresses;
+import android.net.IpPrefix;
+import android.system.OsConstants;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.net.module.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RtNetlinkRouteMessageTest {
+ private static final IpPrefix TEST_IPV6_GLOBAL_PREFIX = new IpPrefix("2001:db8:1::/64");
+ private static final Inet6Address TEST_IPV6_LINK_LOCAL_GATEWAY =
+ (Inet6Address) InetAddresses.parseNumericAddress("fe80::1");
+
+ // An example of the full RTM_NEWROUTE message.
+ private static final String RTM_NEWROUTE_HEX =
+ "88000000180000060000000000000000" // struct nlmsghr
+ + "0A400000FC02000100000000" // struct rtmsg
+ + "08000F00C7060000" // RTA_TABLE
+ + "1400010020010DB8000100000000000000000000" // RTA_DST
+ + "08000400DF020000" // RTA_OIF
+ + "0800060000010000" // RTA_PRIORITY
+ + "24000C0000000000000000005EEA000000000000" // RTA_CACHEINFO
+ + "00000000000000000000000000000000"
+ + "14000500FE800000000000000000000000000001" // RTA_GATEWAY
+ + "0500140000000000"; // RTA_PREF
+
+ private ByteBuffer toByteBuffer(final String hexString) {
+ 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;
+
+ final StructNlMsgHdr hdr = routeMsg.getHeader();
+ assertNotNull(hdr);
+ assertEquals(136, hdr.nlmsg_len);
+ assertEquals(NetlinkConstants.RTM_NEWROUTE, hdr.nlmsg_type);
+ assertEquals(0x600, hdr.nlmsg_flags);
+ assertEquals(0, hdr.nlmsg_seq);
+ assertEquals(0, hdr.nlmsg_pid);
+
+ final StructRtMsg rtmsg = routeMsg.getRtMsgHeader();
+ assertNotNull(rtmsg);
+ assertEquals((byte) OsConstants.AF_INET6, rtmsg.family);
+ assertEquals(64, rtmsg.dstLen);
+ assertEquals(0, rtmsg.srcLen);
+ assertEquals(0, rtmsg.tos);
+ assertEquals(0xFC, rtmsg.table);
+ assertEquals(NetlinkConstants.RTPROT_KERNEL, rtmsg.protocol);
+ assertEquals(NetlinkConstants.RT_SCOPE_UNIVERSE, rtmsg.scope);
+ assertEquals(NetlinkConstants.RTN_UNICAST, rtmsg.type);
+ assertEquals(0, rtmsg.flags);
+
+ assertEquals(routeMsg.getDestination(), TEST_IPV6_GLOBAL_PREFIX);
+ assertEquals(735, routeMsg.getInterfaceIndex());
+ assertEquals((Inet6Address) routeMsg.getGateway(), TEST_IPV6_LINK_LOCAL_GATEWAY);
+ }
+
+ private static final String RTM_NEWROUTE_PACK_HEX =
+ "4C000000180000060000000000000000" // struct nlmsghr
+ + "0A400000FC02000100000000" // struct rtmsg
+ + "1400010020010DB8000100000000000000000000" // RTA_DST
+ + "14000500FE800000000000000000000000000001" // RTA_GATEWAY
+ + "08000400DF020000"; // RTA_OIF
+
+ @Test
+ public void testPackRtmNewRoute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_PACK_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 ByteBuffer packBuffer = ByteBuffer.allocate(76);
+ packBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ routeMsg.pack(packBuffer);
+ assertEquals(RTM_NEWROUTE_PACK_HEX, HexDump.toHexString(packBuffer.array()));
+ }
+
+ private static final String RTM_NEWROUTE_TRUNCATED_HEX =
+ "48000000180000060000000000000000" // struct nlmsghr
+ + "0A400000FC02000100000000" // struct rtmsg
+ + "1400010020010DB8000100000000000000000000" // RTA_DST
+ + "10000500FE8000000000000000000000" // RTA_GATEWAY(truncated)
+ + "08000400DF020000"; // RTA_OIF
+
+ @Test
+ public void testTruncatedRtmNewRoute() {
+ final ByteBuffer byteBuffer = toByteBuffer(RTM_NEWROUTE_TRUNCATED_HEX);
+ byteBuffer.order(ByteOrder.LITTLE_ENDIAN); // For testing.
+ final NetlinkMessage msg = NetlinkMessage.parse(byteBuffer, NETLINK_ROUTE);
+ // Parsing RTM_NEWROUTE with truncated RTA_GATEWAY attribute returns null.
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWROUTE_IPV4_MAPPED_IPV6_GATEWAY_HEX =
+ "4C000000180000060000000000000000" // struct nlmsghr
+ + "0A400000FC02000100000000" // struct rtmsg
+ + "1400010020010DB8000100000000000000000000" // RTA_DST(2001:db8:1::/64)
+ + "1400050000000000000000000000FFFF0A010203" // RTA_GATEWAY(::ffff:10.1.2.3)
+ + "08000400DF020000"; // RTA_OIF
+
+ @Test
+ public void testParseRtmRouteAddress_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);
+ // Parsing RTM_NEWROUTE with IPv4-mapped IPv6 gateway address, which doesn't match
+ // rtm_family after address parsing.
+ assertNull(msg);
+ }
+
+ private static final String RTM_NEWROUTE_IPV4_MAPPED_IPV6_DST_HEX =
+ "4C000000180000060000000000000000" // struct nlmsghr
+ + "0A780000FC02000100000000" // struct rtmsg
+ + "1400010000000000000000000000FFFF0A000000" // RTA_DST(::ffff:10.0.0.0/120)
+ + "14000500FE800000000000000000000000000001" // RTA_GATEWAY(fe80::1)
+ + "08000400DF020000"; // RTA_OIF
+
+ @Test
+ public void testParseRtmRouteAddress_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);
+ // Parsing RTM_NEWROUTE with IPv4-mapped IPv6 destination prefix, which doesn't match
+ // rtm_family after address parsing.
+ assertNull(msg);
+ }
+
+ @Test
+ public void testToString() {
+ 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;
+ final String expected = "RtNetlinkRouteMessage{ "
+ + "nlmsghdr{"
+ + "StructNlMsgHdr{ nlmsg_len{136}, nlmsg_type{24(RTM_NEWROUTE)}, "
+ + "nlmsg_flags{1536(NLM_F_MATCH)}, nlmsg_seq{0}, nlmsg_pid{0} }}, "
+ + "Rtmsg{"
+ + "family: 10, dstLen: 64, srcLen: 0, tos: 0, table: 252, protocol: 2, "
+ + "scope: 0, type: 1, flags: 0}, "
+ + "destination{2001:db8:1::}, "
+ + "gateway{fe80::1}, "
+ + "ifindex{735} "
+ + "}";
+ assertEquals(expected, routeMsg.toString());
+ }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index b7297bb..9fd30f7 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -61,3 +61,14 @@
"kotlin-test"
]
}
+
+java_test_host {
+ name: "net-tests-utils-host-common",
+ srcs: [
+ "host/**/*.java",
+ "host/**/*.kt",
+ ],
+ libs: ["tradefed"],
+ test_suites: ["device-tests", "general-tests", "cts", "mts"],
+ data: [":ConnectivityChecker"],
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
new file mode 100644
index 0000000..79a4343
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2021 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test_helper_app {
+ name: "ConnectivityChecker",
+ srcs: ["src/**/*.kt"],
+ sdk_version: "system_current",
+ // Allow running the test on any device with SDK Q+, even when built from a branch that uses
+ // an unstable SDK, by targeting a stable SDK regardless of the build SDK.
+ min_sdk_version: "29",
+ target_sdk_version: "30",
+ static_libs: [
+ "androidx.test.rules",
+ "modules-utils-build_system",
+ "net-tests-utils",
+ ],
+ host_required: ["net-tests-utils-host-common"],
+}
diff --git a/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
new file mode 100644
index 0000000..8e5958c
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.testutils.connectivitychecker">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <!-- For wifi scans -->
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+ <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.testutils.connectivitychecker"
+ android:label="Connectivity checker target preparer" />
+</manifest>
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
new file mode 100644
index 0000000..43b130b
--- /dev/null
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitychecker/ConnectivityCheckTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2021 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.connectivitychecker
+
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.telephony.TelephonyManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.testutils.ConnectUtil
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+@RunWith(AndroidJUnit4::class)
+class ConnectivityCheckTest {
+ val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ val pm by lazy { context.packageManager }
+
+ @Test
+ fun testCheckDeviceSetup() {
+ checkWifiSetup()
+ checkTelephonySetup()
+ }
+
+ private fun checkWifiSetup() {
+ if (!pm.hasSystemFeature(FEATURE_WIFI)) return
+ ConnectUtil(context).ensureWifiConnected()
+ }
+
+ private fun checkTelephonySetup() {
+ if (!pm.hasSystemFeature(FEATURE_TELEPHONY)) return
+ val tm = context.getSystemService(TelephonyManager::class.java)
+ ?: fail("Could not get telephony service")
+
+ val commonError = "Check the test bench. To run the tests anyway for quick & dirty local " +
+ "testing, you can use atest X -- " +
+ "--test-arg com.android.testutils.ConnectivityCheckTargetPreparer:disable:true"
+ // Do not use assertEquals: it outputs "expected X, was Y", which looks like a test failure
+ if (tm.simState == TelephonyManager.SIM_STATE_ABSENT) {
+ fail("The device has no SIM card inserted. " + commonError)
+ } else if (tm.simState != TelephonyManager.SIM_STATE_READY) {
+ fail("The device is not setup with a usable SIM card. Sim state was ${tm.simState}. " +
+ commonError)
+ }
+ assertTrue(tm.isDataConnectivityPossible,
+ "The device is not setup with a SIM card that supports data connectivity. " +
+ commonError)
+ }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
new file mode 100644
index 0000000..fc951d8
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2021 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 android.Manifest.permission
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.ScanResult
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.os.ParcelFileDescriptor
+import android.os.SystemClock
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+private const val MAX_WIFI_CONNECT_RETRIES = 10
+private const val WIFI_CONNECT_INTERVAL_MS = 500L
+private const val WIFI_CONNECT_TIMEOUT_MS = 30_000L
+
+// Constants used by WifiManager.ActionListener#onFailure. Although onFailure is SystemApi,
+// the error code constants are not (b/204277752)
+private const val WIFI_ERROR_IN_PROGRESS = 1
+private const val WIFI_ERROR_BUSY = 2
+
+class ConnectUtil(private val context: Context) {
+ private val TAG = ConnectUtil::class.java.simpleName
+
+ private val cm = context.getSystemService(ConnectivityManager::class.java)
+ ?: fail("Could not find ConnectivityManager")
+ private val wifiManager = context.getSystemService(WifiManager::class.java)
+ ?: fail("Could not find WifiManager")
+
+ fun ensureWifiConnected(): Network {
+ val callback = TestableNetworkCallback()
+ cm.registerNetworkCallback(NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build(), callback)
+
+ try {
+ val connInfo = wifiManager.connectionInfo
+ if (connInfo == null || connInfo.networkId == -1) {
+ clearWifiBlocklist()
+ val pfd = getInstrumentation().uiAutomation.executeShellCommand("svc wifi enable")
+ // Read the output stream to ensure the command has completed
+ ParcelFileDescriptor.AutoCloseInputStream(pfd).use { it.readBytes() }
+ val config = getOrCreateWifiConfiguration()
+ connectToWifiConfig(config)
+ }
+ val cb = callback.eventuallyExpectOrNull<RecorderCallback.CallbackEntry.Available>(
+ timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
+
+ assertNotNull(cb, "Could not connect to a wifi access point within " +
+ "$WIFI_CONNECT_INTERVAL_MS ms. Check that the test device has a wifi network " +
+ "configured, and that the test access point is functioning properly.")
+ return cb.network
+ } finally {
+ cm.unregisterNetworkCallback(callback)
+ }
+ }
+
+ private fun connectToWifiConfig(config: WifiConfiguration) {
+ repeat(MAX_WIFI_CONNECT_RETRIES) {
+ val error = runAsShell(permission.NETWORK_SETTINGS) {
+ val listener = ConnectWifiListener()
+ wifiManager.connect(config, listener)
+ listener.connectFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ } ?: return // Connect succeeded
+
+ // Only retry for IN_PROGRESS and BUSY
+ if (error != WIFI_ERROR_IN_PROGRESS && error != WIFI_ERROR_BUSY) {
+ fail("Failed to connect to " + config.SSID + ": " + error)
+ }
+ Log.w(TAG, "connect failed with $error; waiting before retry")
+ SystemClock.sleep(WIFI_CONNECT_INTERVAL_MS)
+ }
+ fail("Failed to connect to ${config.SSID} after $MAX_WIFI_CONNECT_RETRIES retries")
+ }
+
+ private class ConnectWifiListener : WifiManager.ActionListener {
+ /**
+ * Future completed when the connect process ends. Provides the error code or null if none.
+ */
+ val connectFuture = CompletableFuture<Int?>()
+ override fun onSuccess() {
+ connectFuture.complete(null)
+ }
+
+ override fun onFailure(reason: Int) {
+ connectFuture.complete(reason)
+ }
+ }
+
+ private fun getOrCreateWifiConfiguration(): WifiConfiguration {
+ val configs = runAsShell(permission.NETWORK_SETTINGS) {
+ wifiManager.getConfiguredNetworks()
+ }
+ // If no network is configured, add a config for virtual access points if applicable
+ if (configs.size == 0) {
+ val scanResults = getWifiScanResults()
+ val virtualConfig = maybeConfigureVirtualNetwork(scanResults)
+ assertNotNull(virtualConfig, "The device has no configured wifi network")
+ return virtualConfig
+ }
+ // No need to add a configuration: there is already one.
+ if (configs.size > 1) {
+ // For convenience in case of local testing on devices with multiple saved configs,
+ // prefer the first configuration that is in range.
+ // In actual tests, there should only be one configuration, and it should be usable as
+ // assumed by WifiManagerTest.testConnect.
+ Log.w(TAG, "Multiple wifi configurations found: " +
+ configs.joinToString(", ") { it.SSID })
+ val scanResultsList = getWifiScanResults()
+ Log.i(TAG, "Scan results: " + scanResultsList.joinToString(", ") {
+ "${it.SSID} (${it.level})"
+ })
+
+ val scanResults = scanResultsList.map { "\"${it.SSID}\"" }.toSet()
+ return configs.firstOrNull { scanResults.contains(it.SSID) } ?: configs[0]
+ }
+ return configs[0]
+ }
+
+ private fun getWifiScanResults(): List<ScanResult> {
+ val scanResultsFuture = CompletableFuture<List<ScanResult>>()
+ runAsShell(permission.NETWORK_SETTINGS) {
+ val receiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ scanResultsFuture.complete(wifiManager.scanResults)
+ }
+ }
+ context.registerReceiver(receiver,
+ IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
+ wifiManager.startScan()
+ }
+ return try {
+ scanResultsFuture.get(WIFI_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ } catch (e: Exception) {
+ throw AssertionError("Wifi scan results not received within timeout", e)
+ }
+ }
+
+ /**
+ * If a virtual wifi network is detected, add a configuration for that network.
+ * TODO(b/158150376): have the test infrastructure add virtual wifi networks when appropriate.
+ */
+ private fun maybeConfigureVirtualNetwork(scanResults: List<ScanResult>): WifiConfiguration? {
+ // Virtual wifi networks used on the emulator and cloud testing infrastructure
+ val virtualSsids = listOf("VirtWifi", "AndroidWifi")
+ Log.d(TAG, "Wifi scan results: $scanResults")
+ val virtualScanResult = scanResults.firstOrNull { virtualSsids.contains(it.SSID) }
+ ?: return null
+
+ // Only add the virtual configuration if the virtual AP is detected in scans
+ val virtualConfig = WifiConfiguration()
+ // ASCII SSIDs need to be surrounded by double quotes
+ virtualConfig.SSID = "\"${virtualScanResult.SSID}\""
+ virtualConfig.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE)
+ runAsShell(permission.NETWORK_SETTINGS) {
+ val networkId = wifiManager.addNetwork(virtualConfig)
+ assertTrue(networkId >= 0)
+ assertTrue(wifiManager.enableNetwork(networkId, false /* attemptConnect */))
+ }
+ return virtualConfig
+ }
+
+ /**
+ * Re-enable wifi networks that were blocked, typically because no internet connection was
+ * detected the last time they were connected. This is necessary to make sure wifi can reconnect
+ * to them.
+ */
+ private fun clearWifiBlocklist() {
+ runAsShell(permission.NETWORK_SETTINGS, permission.ACCESS_WIFI_STATE) {
+ for (cfg in wifiManager.configuredNetworks) {
+ assertTrue(wifiManager.enableNetwork(cfg.networkId, false /* attemptConnect */))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/staticlibs/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt b/staticlibs/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt
new file mode 100644
index 0000000..85589ad
--- /dev/null
+++ b/staticlibs/testutils/host/com/android/testutils/ConnectivityCheckTargetPreparer.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 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 com.android.ddmlib.testrunner.TestResult
+import com.android.tradefed.invoker.TestInformation
+import com.android.tradefed.result.CollectingTestListener
+import com.android.tradefed.result.ddmlib.DefaultRemoteAndroidTestRunner
+import com.android.tradefed.targetprep.BaseTargetPreparer
+import com.android.tradefed.targetprep.suite.SuiteApkInstaller
+
+private const val CONNECTIVITY_CHECKER_APK = "ConnectivityChecker.apk"
+private const val CONNECTIVITY_PKG_NAME = "com.android.testutils.connectivitychecker"
+// As per the <instrumentation> defined in the checker manifest
+private const val CONNECTIVITY_CHECK_RUNNER_NAME = "androidx.test.runner.AndroidJUnitRunner"
+
+/**
+ * A target preparer that verifies that the device was setup correctly for connectivity tests.
+ *
+ * For quick and dirty local testing, can be disabled by running tests with
+ * "atest -- --test-arg com.android.testutils.ConnectivityCheckTargetPreparer:disable:true".
+ */
+class ConnectivityCheckTargetPreparer : BaseTargetPreparer() {
+ val installer = SuiteApkInstaller()
+
+ override fun setUp(testInformation: TestInformation) {
+ if (isDisabled) return
+ installer.setCleanApk(true)
+ installer.addTestFileName(CONNECTIVITY_CHECKER_APK)
+ installer.setShouldGrantPermission(true)
+ installer.setUp(testInformation)
+
+ val runner = DefaultRemoteAndroidTestRunner(
+ CONNECTIVITY_PKG_NAME,
+ CONNECTIVITY_CHECK_RUNNER_NAME,
+ testInformation.device.iDevice)
+ runner.runOptions = "--no-hidden-api-checks"
+
+ val receiver = CollectingTestListener()
+ if (!testInformation.device.runInstrumentationTests(runner, receiver)) {
+ throw AssertionError("Device state check failed to complete")
+ }
+
+ val runResult = receiver.currentRunResults
+ if (runResult.isRunFailure) {
+ throw AssertionError("Failed to check device state before the test: " +
+ runResult.runFailureMessage)
+ }
+
+ if (!runResult.hasFailedTests()) return
+ val errorMsg = runResult.testResults.mapNotNull { (testDescription, testResult) ->
+ if (TestResult.TestStatus.FAILURE != testResult.status) null
+ else "$testDescription: ${testResult.stackTrace}"
+ }.joinToString("\n")
+
+ throw AssertionError("Device setup checks failed. Check the test bench: \n$errorMsg")
+ }
+
+ override fun tearDown(testInformation: TestInformation?, e: Throwable?) {
+ if (isTearDownDisabled) return
+ installer.tearDown(testInformation, e)
+ }
+}
\ No newline at end of file