Merge changes I25158126,I99fcf77b
* changes:
TrafficControllerTest - trivial simplification
simplify bpf tests and check type correctness
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 85072a7..4eeaf51 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -34,6 +34,18 @@
}
]
},
+ // CTS tests that target older SDKs.
+ {
+ "name": "CtsNetTestCasesMaxTargetSdk31",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
{
"name": "bpf_existence_test"
},
@@ -41,6 +53,9 @@
"name": "connectivity_native_test"
},
{
+ "name": "libclat_test"
+ },
+ {
"name": "netd_updatable_unit_test"
},
{
@@ -68,9 +83,6 @@
"keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
},
{
- "name": "libclat_test"
- },
- {
"name": "traffic_controller_unit_test",
"keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
},
@@ -91,6 +103,17 @@
]
},
{
+ "name": "CtsNetTestCasesMaxTargetSdk31[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]",
+ "options": [
+ {
+ "exclude-annotation": "com.android.testutils.SkipPresubmit"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.RequiresDevice"
+ }
+ ]
+ },
+ {
"name": "bpf_existence_test[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]"
},
{
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 026ed54..adcc236 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -21,7 +21,7 @@
java_defaults {
name: "TetheringApiLevel",
sdk_version: "module_current",
- target_sdk_version: "31",
+ target_sdk_version: "33",
min_sdk_version: "30",
}
diff --git a/Tethering/apex/manifest.json b/Tethering/apex/manifest.json
index 88f13b2..3cb03ed 100644
--- a/Tethering/apex/manifest.json
+++ b/Tethering/apex/manifest.json
@@ -1,4 +1,4 @@
{
"name": "com.android.tethering",
- "version": 319999900
+ "version": 339990000
}
diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp
index a4d0448..31c3df3 100644
--- a/Tethering/tests/integration/Android.bp
+++ b/Tethering/tests/integration/Android.bp
@@ -49,7 +49,7 @@
// Use with NetworkStackJarJarRules.
android_library {
name: "TetheringIntegrationTestsLatestSdkLib",
- target_sdk_version: "31",
+ target_sdk_version: "33",
platform_apis: true,
defaults: ["TetheringIntegrationTestsDefaults"],
visibility: [
diff --git a/Tethering/tests/mts/Android.bp b/Tethering/tests/mts/Android.bp
index 18fd63b..a84fdd2 100644
--- a/Tethering/tests/mts/Android.bp
+++ b/Tethering/tests/mts/Android.bp
@@ -22,7 +22,7 @@
name: "MtsTetheringTestLatestSdk",
min_sdk_version: "30",
- target_sdk_version: "31",
+ target_sdk_version: "33",
libs: [
"android.test.base",
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index d1b8380..0ee12ad 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -89,7 +89,7 @@
static_libs: [
"TetheringApiStableLib",
],
- target_sdk_version: "31",
+ target_sdk_version: "33",
visibility: [
"//packages/modules/Connectivity/tests:__subpackages__",
"//packages/modules/Connectivity/Tethering/tests:__subpackages__",
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 14fcdd6..634fbf4 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -147,9 +147,9 @@
SELECT_MAP_B,
};
-// TODO: change the configuration object from an 8-bit bitmask to an object with clearer
+// TODO: change the configuration object from a bitmask to an object with clearer
// semantics, like a struct.
-typedef uint8_t BpfConfig;
+typedef uint32_t BpfConfig;
static const BpfConfig DEFAULT_CONFIG = 0;
typedef struct {
@@ -160,7 +160,9 @@
} UidOwnerValue;
STRUCT_SIZE(UidOwnerValue, 2 * 4); // 8
+// Entry in the configuration map that stores which UID rules are enabled.
#define UID_RULES_CONFIGURATION_KEY 1
+// Entry in the configuration map that stores which stats map is currently in use.
#define CURRENT_STATS_MAP_CONFIGURATION_KEY 2
#define BPF_CLATD_PATH "/sys/fs/bpf/net_shared/"
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index ae92686..2a63e81 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -62,7 +62,7 @@
DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint8_t, CONFIGURATION_MAP_SIZE,
+DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE,
AID_NET_BW_ACCT)
DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
AID_NET_BW_ACCT)
@@ -234,7 +234,7 @@
}
static __always_inline inline void update_stats_with_config(struct __sk_buff* skb, int direction,
- StatsKey* key, uint8_t selectedMap) {
+ StatsKey* key, uint32_t selectedMap) {
if (selectedMap == SELECT_MAP_A) {
update_stats_map_A(skb, direction, key);
} else if (selectedMap == SELECT_MAP_B) {
@@ -286,7 +286,7 @@
if (counterSet) key.counterSet = (uint32_t)*counterSet;
uint32_t mapSettingKey = CURRENT_STATS_MAP_CONFIGURATION_KEY;
- uint8_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
+ uint32_t* selectedMap = bpf_configuration_map_lookup_elem(&mapSettingKey);
// Use asm("%0 &= 1" : "+r"(match)) before return match,
// to help kernel's bpf verifier, so that it can be 100% certain
diff --git a/framework-t/src/android/net/NetworkStatsCollection.java b/framework-t/src/android/net/NetworkStatsCollection.java
index 29ea772..6a1d2dd 100644
--- a/framework-t/src/android/net/NetworkStatsCollection.java
+++ b/framework-t/src/android/net/NetworkStatsCollection.java
@@ -865,6 +865,9 @@
* Add association of the history with the specified key in this map.
*
* @param key The object used to identify a network, see {@link Key}.
+ * If history already exists for this key, then the passed-in history is appended
+ * to the previously-passed in history. The caller must ensure that the history
+ * passed-in timestamps are greater than all previously-passed-in timestamps.
* @param history {@link NetworkStatsHistory} instance associated to the given {@link Key}.
* @return The builder object.
*/
@@ -874,9 +877,21 @@
Objects.requireNonNull(key);
Objects.requireNonNull(history);
final List<Entry> historyEntries = history.getEntries();
+ final NetworkStatsHistory existing = mEntries.get(key);
+ final int size = historyEntries.size() + ((existing != null) ? existing.size() : 0);
final NetworkStatsHistory.Builder historyBuilder =
- new NetworkStatsHistory.Builder(mBucketDurationMillis, historyEntries.size());
+ new NetworkStatsHistory.Builder(mBucketDurationMillis, size);
+
+ // TODO: this simply appends the entries to any entries that were already present in
+ // the builder, which requires the caller to pass in entries in order. We might be
+ // able to do better with something like recordHistory.
+ if (existing != null) {
+ for (Entry entry : existing.getEntries()) {
+ historyBuilder.addEntry(entry);
+ }
+ }
+
for (Entry entry : historyEntries) {
historyBuilder.addEntry(entry);
}
diff --git a/framework-t/src/android/net/NetworkStatsHistory.java b/framework-t/src/android/net/NetworkStatsHistory.java
index b45d44d..0ff9d96 100644
--- a/framework-t/src/android/net/NetworkStatsHistory.java
+++ b/framework-t/src/android/net/NetworkStatsHistory.java
@@ -32,6 +32,7 @@
import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
@@ -949,6 +950,25 @@
return writer.toString();
}
+ /**
+ * Same as "equals", but not actually called equals as this would affect public API behavior.
+ * @hide
+ */
+ @Nullable
+ public boolean isSameAs(NetworkStatsHistory other) {
+ return bucketCount == other.bucketCount
+ && Arrays.equals(bucketStart, other.bucketStart)
+ // Don't check activeTime since it can change on import due to the importer using
+ // recordHistory. It's also not exposed by the APIs or present in dumpsys or
+ // toString().
+ && Arrays.equals(rxBytes, other.rxBytes)
+ && Arrays.equals(rxPackets, other.rxPackets)
+ && Arrays.equals(txBytes, other.txBytes)
+ && Arrays.equals(txPackets, other.txPackets)
+ && Arrays.equals(operations, other.operations)
+ && totalBytes == other.totalBytes;
+ }
+
@UnsupportedAppUsage
public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
@Override
@@ -1116,14 +1136,44 @@
mOperations = new ArrayList<>(initialCapacity);
}
+ private void addToElement(List<Long> list, int pos, long value) {
+ list.set(pos, list.get(pos) + value);
+ }
+
/**
* Add an {@link Entry} into the {@link NetworkStatsHistory} instance.
*
- * @param entry The target {@link Entry} object.
+ * @param entry The target {@link Entry} object. The entry timestamp must be greater than
+ * that of any previously-added entry.
* @return The builder object.
*/
@NonNull
public Builder addEntry(@NonNull Entry entry) {
+ final int lastBucket = mBucketStart.size() - 1;
+ final long lastBucketStart = (lastBucket != -1) ? mBucketStart.get(lastBucket) : 0;
+
+ // If last bucket has the same timestamp, modify it instead of adding another bucket.
+ // This allows callers to pass in the same bucket twice (e.g., to accumulate
+ // data over time), but still requires that entries must be sorted.
+ // The importer will do this in case a rotated file has the same timestamp as
+ // the previous file.
+ if (lastBucket != -1 && entry.bucketStart == lastBucketStart) {
+ addToElement(mActiveTime, lastBucket, entry.activeTime);
+ addToElement(mRxBytes, lastBucket, entry.rxBytes);
+ addToElement(mRxPackets, lastBucket, entry.rxPackets);
+ addToElement(mTxBytes, lastBucket, entry.txBytes);
+ addToElement(mTxPackets, lastBucket, entry.txPackets);
+ addToElement(mOperations, lastBucket, entry.operations);
+ return this;
+ }
+
+ // Inserting in the middle is prohibited for performance reasons.
+ if (entry.bucketStart <= lastBucketStart) {
+ throw new IllegalArgumentException("new bucket start " + entry.bucketStart
+ + " must be greater than last bucket start " + lastBucketStart);
+ }
+
+ // Common case: add entries at the end of the list.
mBucketStart.add(entry.bucketStart);
mActiveTime.add(entry.activeTime);
mRxBytes.add(entry.rxBytes);
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 2621594..200c808 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -53,6 +53,8 @@
@Nullable
private Network mNetwork;
+ private int mInterfaceIndex;
+
public NsdServiceInfo() {
}
@@ -312,8 +314,11 @@
/**
* Get the network where the service can be found.
*
- * This is never null if this {@link NsdServiceInfo} was obtained from
- * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}.
+ * This is set if this {@link NsdServiceInfo} was obtained from
+ * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}, unless the service
+ * was found on a network interface that does not have a {@link Network} (such as a tethering
+ * downstream, where services are advertised from devices connected to this device via
+ * tethering).
*/
@Nullable
public Network getNetwork() {
@@ -329,6 +334,26 @@
mNetwork = network;
}
+ /**
+ * Get the index of the network interface where the service was found.
+ *
+ * This is only set when the service was found on an interface that does not have a usable
+ * Network, in which case {@link #getNetwork()} returns null.
+ * @return The interface index as per {@link java.net.NetworkInterface#getIndex}, or 0 if unset.
+ * @hide
+ */
+ public int getInterfaceIndex() {
+ return mInterfaceIndex;
+ }
+
+ /**
+ * Set the index of the network interface where the service was found.
+ * @hide
+ */
+ public void setInterfaceIndex(int interfaceIndex) {
+ mInterfaceIndex = interfaceIndex;
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -375,6 +400,7 @@
}
dest.writeParcelable(mNetwork, 0);
+ dest.writeInt(mInterfaceIndex);
}
/** Implement the Parcelable interface */
@@ -405,6 +431,7 @@
info.mTxtRecord.put(in.readString(), valueArray);
}
info.mNetwork = in.readParcelable(null, Network.class);
+ info.mInterfaceIndex = in.readInt();
return info;
}
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index 2ede1c1..05b9ebc 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -62,7 +62,7 @@
BpfMap<uint64_t, UidTagValue> mCookieTagMap;
BpfMap<StatsKey, StatsValue> mStatsMapA;
BpfMap<StatsKey, StatsValue> mStatsMapB;
- BpfMap<uint32_t, uint8_t> mConfigurationMap;
+ BpfMap<uint32_t, uint32_t> mConfigurationMap;
BpfMap<uint32_t, uint8_t> mUidPermissionMap;
std::mutex mMutex;
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index 77f1a44..fae3bc4 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -48,7 +48,7 @@
BpfHandler mBh;
BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
- BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+ BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
void SetUp() {
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
index 6c7a15e..c67821f 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStats.cpp
@@ -193,7 +193,7 @@
return ret;
}
- BpfMapRO<uint32_t, uint8_t> configurationMap(CONFIGURATION_MAP_PATH);
+ BpfMapRO<uint32_t, uint32_t> configurationMap(CONFIGURATION_MAP_PATH);
if (!configurationMap.isValid()) {
int ret = -errno;
ALOGE("get configuration map fd failed: %s", strerror(errno));
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 6def44f..95e6114 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -23,6 +23,7 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
+import android.net.INetd;
import android.net.LinkProperties;
import android.net.Network;
import android.net.mdns.aidl.DiscoveryInfo;
@@ -466,7 +467,7 @@
// interfaces that do not have an associated Network.
break;
}
- servInfo.setNetwork(new Network(foundNetId));
+ setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
clientInfo.onServiceFound(clientId, servInfo);
break;
}
@@ -476,10 +477,11 @@
final String type = info.registrationType;
final int lostNetId = info.netId;
servInfo = new NsdServiceInfo(name, type);
- // The network could be null if it was torn down when the service is lost
- // TODO: avoid returning null in that case, possibly by remembering found
- // services on the same interface index and their network at the time
- servInfo.setNetwork(lostNetId == 0 ? null : new Network(lostNetId));
+ // The network could be set to null (netId 0) if it was torn down when the
+ // service is lost
+ // TODO: avoid returning null in that case, possibly by remembering
+ // found services on the same interface index and their network at the time
+ setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
clientInfo.onServiceLost(clientId, servInfo);
break;
}
@@ -557,7 +559,6 @@
final GetAddressInfo info = (GetAddressInfo) obj;
final String address = info.address;
final int netId = info.netId;
- final Network network = netId == NETID_UNSET ? null : new Network(netId);
InetAddress serviceHost = null;
try {
serviceHost = InetAddress.getByName(address);
@@ -568,9 +569,10 @@
// If the resolved service is on an interface without a network, consider it
// as a failure: it would not be usable by apps as they would need
// privileged permissions.
- if (network != null && serviceHost != null) {
+ if (netId != NETID_UNSET && serviceHost != null) {
clientInfo.mResolvedService.setHost(serviceHost);
- clientInfo.mResolvedService.setNetwork(network);
+ setServiceNetworkForCallback(clientInfo.mResolvedService,
+ netId, info.interfaceIdx);
clientInfo.onResolveServiceSucceeded(
clientId, clientInfo.mResolvedService);
} else {
@@ -590,6 +592,26 @@
}
}
+ private static void setServiceNetworkForCallback(NsdServiceInfo info, int netId, int ifaceIdx) {
+ switch (netId) {
+ case NETID_UNSET:
+ info.setNetwork(null);
+ break;
+ case INetd.LOCAL_NET_ID:
+ // Special case for LOCAL_NET_ID: Networks on netId 99 are not generally
+ // visible / usable for apps, so do not return it. Store the interface
+ // index instead, so at least if the client tries to resolve the service
+ // with that NsdServiceInfo, it will be done on the same interface.
+ // If they recreate the NsdServiceInfo themselves, resolution would be
+ // done on all interfaces as before T, which should also work.
+ info.setNetwork(null);
+ info.setInterfaceIndex(ifaceIdx);
+ break;
+ default:
+ info.setNetwork(new Network(netId));
+ }
+ }
+
// The full service name is escaped from standard DNS rules on mdnsresponder, making it suitable
// for passing to standard system DNS APIs such as res_query() . Thus, make the service name
// unescape for getting right service address. See "Notes on DNS Name Escaping" on
@@ -767,9 +789,8 @@
String type = service.getServiceType();
int port = service.getPort();
byte[] textRecord = service.getTxtRecord();
- final Network network = service.getNetwork();
- final int registerInterface = getNetworkInterfaceIndex(network);
- if (network != null && registerInterface == IFACE_IDX_ANY) {
+ final int registerInterface = getNetworkInterfaceIndex(service);
+ if (service.getNetwork() != null && registerInterface == IFACE_IDX_ANY) {
Log.e(TAG, "Interface to register service on not found");
return false;
}
@@ -781,10 +802,9 @@
}
private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
- final Network network = serviceInfo.getNetwork();
final String type = serviceInfo.getServiceType();
- final int discoverInterface = getNetworkInterfaceIndex(network);
- if (network != null && discoverInterface == IFACE_IDX_ANY) {
+ final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
+ if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
Log.e(TAG, "Interface to discover service on not found");
return false;
}
@@ -798,9 +818,8 @@
private boolean resolveService(int resolveId, NsdServiceInfo service) {
final String name = service.getServiceName();
final String type = service.getServiceType();
- final Network network = service.getNetwork();
- final int resolveInterface = getNetworkInterfaceIndex(network);
- if (network != null && resolveInterface == IFACE_IDX_ANY) {
+ final int resolveInterface = getNetworkInterfaceIndex(service);
+ if (service.getNetwork() != null && resolveInterface == IFACE_IDX_ANY) {
Log.e(TAG, "Interface to resolve service on not found");
return false;
}
@@ -816,8 +835,17 @@
* this is to support the legacy mdnsresponder implementation, which historically resolved
* services on an unspecified network.
*/
- private int getNetworkInterfaceIndex(Network network) {
- if (network == null) return IFACE_IDX_ANY;
+ private int getNetworkInterfaceIndex(NsdServiceInfo serviceInfo) {
+ final Network network = serviceInfo.getNetwork();
+ if (network == null) {
+ // Fallback to getInterfaceIndex if present (typically if the NsdServiceInfo was
+ // provided by NsdService from discovery results, and the service was found on an
+ // interface that has no app-usable Network).
+ if (serviceInfo.getInterfaceIndex() != 0) {
+ return serviceInfo.getInterfaceIndex();
+ }
+ return IFACE_IDX_ANY;
+ }
final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
if (cm == null) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index 6f070d7..d73e342 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -21,6 +21,7 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+import android.annotation.NonNull;
import android.net.NetworkIdentitySet;
import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver;
@@ -68,7 +69,7 @@
private static final String TAG_NETSTATS_DUMP = "netstats_dump";
- /** Dump before deleting in {@link #recoverFromWtf()}. */
+ /** Dump before deleting in {@link #recoverAndDeleteData()}. */
private static final boolean DUMP_BEFORE_DELETE = true;
private final FileRotator mRotator;
@@ -156,6 +157,15 @@
return mSinceBoot;
}
+ public long getBucketDuration() {
+ return mBucketDuration;
+ }
+
+ @NonNull
+ public String getCookie() {
+ return mCookie;
+ }
+
/**
* Load complete history represented by {@link FileRotator}. Caches
* internally as a {@link WeakReference}, and updated with future
@@ -189,10 +199,10 @@
res.recordCollection(mPending);
} catch (IOException e) {
Log.wtf(TAG, "problem completely reading network stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem completely reading network stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
}
return res;
}
@@ -300,10 +310,10 @@
mPending.reset();
} catch (IOException e) {
Log.wtf(TAG, "problem persisting pending stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem persisting pending stats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
}
}
}
@@ -319,10 +329,10 @@
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
} catch (IOException e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
- recoverFromWtf();
+ recoverAndDeleteData();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
- recoverFromWtf();
+ recoverAndDeleteData();
}
}
@@ -347,8 +357,7 @@
/**
* Rewriter that will combine current {@link NetworkStatsCollection} values
- * with anything read from disk, and write combined set to disk. Clears the
- * original {@link NetworkStatsCollection} when finished writing.
+ * with anything read from disk, and write combined set to disk.
*/
private static class CombiningRewriter implements FileRotator.Rewriter {
private final NetworkStatsCollection mCollection;
@@ -375,7 +384,6 @@
@Override
public void write(OutputStream out) throws IOException {
mCollection.write(out);
- mCollection.reset();
}
}
@@ -456,6 +464,23 @@
}
/**
+ * Import a specified {@link NetworkStatsCollection} instance into this recorder,
+ * and write it into a standalone file.
+ * @param collection The target {@link NetworkStatsCollection} instance to be imported.
+ */
+ public void importCollectionLocked(@NonNull NetworkStatsCollection collection)
+ throws IOException {
+ if (mRotator != null) {
+ mRotator.rewriteSingle(new CombiningRewriter(collection), collection.getStartMillis(),
+ collection.getEndMillis());
+ }
+
+ if (mComplete != null) {
+ throw new IllegalStateException("cannot import data when data already loaded");
+ }
+ }
+
+ /**
* Rewriter that will remove any histories or persisted data points before the
* specified cutoff time, only writing data back when modified.
*/
@@ -501,10 +526,10 @@
mBucketDuration, cutoffMillis));
} catch (IOException e) {
Log.wtf(TAG, "problem importing netstats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
} catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem importing netstats", e);
- recoverFromWtf();
+ recoverAndDeleteData();
}
}
@@ -555,7 +580,7 @@
* Recover from {@link FileRotator} failure by dumping state to
* {@link DropBoxManager} and deleting contents.
*/
- private void recoverFromWtf() {
+ void recoverAndDeleteData() {
if (DUMP_BEFORE_DELETE) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index a015177..63e6501 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -67,6 +67,7 @@
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.usage.NetworkStatsManager;
+import android.content.ApexEnvironment;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -75,6 +76,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
+import android.net.ConnectivityManager;
import android.net.DataUsageRequest;
import android.net.INetd;
import android.net.INetworkStatsService;
@@ -100,6 +102,7 @@
import android.net.UnderlyingNetworkInfo;
import android.net.Uri;
import android.net.netstats.IUsageCallback;
+import android.net.netstats.NetworkStatsDataMigrationUtils;
import android.net.netstats.provider.INetworkStatsProvider;
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.netstats.provider.NetworkStatsProvider;
@@ -118,6 +121,7 @@
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
+import android.provider.DeviceConfig;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.service.NetworkInterfaceProto;
@@ -143,6 +147,7 @@
import com.android.net.module.util.BinderUtils;
import com.android.net.module.util.BpfMap;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
import com.android.net.module.util.NetworkStatsUtils;
@@ -155,7 +160,9 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
+import java.nio.file.Path;
import java.time.Clock;
+import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
@@ -232,6 +239,22 @@
private static final String STATS_MAP_B_PATH =
"/sys/fs/bpf/netd_shared/map_netd_stats_map_B";
+ /**
+ * DeviceConfig flag used to indicate whether the files should be stored in the apex data
+ * directory.
+ */
+ static final String NETSTATS_STORE_FILES_IN_APEXDATA = "netstats_store_files_in_apexdata";
+ /**
+ * DeviceConfig flag is used to indicate whether the legacy files need to be imported, and
+ * retry count before giving up. Only valid when {@link #NETSTATS_STORE_FILES_IN_APEXDATA}
+ * set to true. Note that the value gets rollback when the mainline module gets rollback.
+ */
+ static final String NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS =
+ "netstats_import_legacy_target_attempts";
+ static final int DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = 1;
+ static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
+ static final String NETSTATS_IMPORT_SUCCESS_COUNTER_NAME = "import.successes";
+
private final Context mContext;
private final NetworkStatsFactory mStatsFactory;
private final AlarmManager mAlarmManager;
@@ -239,8 +262,7 @@
private final NetworkStatsSettings mSettings;
private final NetworkStatsObservers mStatsObservers;
- private final File mSystemDir;
- private final File mBaseDir;
+ private final File mStatsDir;
private final PowerManager.WakeLock mWakeLock;
@@ -250,6 +272,12 @@
protected INetd mNetd;
private final AlertObserver mAlertObserver = new AlertObserver();
+ // Persistent counters that backed by AtomicFile which stored in the data directory as a file,
+ // to track attempts/successes count across reboot. Note that these counter values will be
+ // rollback as the module rollbacks.
+ private PersistentInt mImportLegacyAttemptsCounter = null;
+ private PersistentInt mImportLegacySuccessesCounter = null;
+
@VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL =
"com.android.server.action.NETWORK_STATS_POLL";
@@ -405,16 +433,6 @@
@NonNull
private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
- private static @NonNull File getDefaultSystemDir() {
- return new File(Environment.getDataDirectory(), "system");
- }
-
- private static @NonNull File getDefaultBaseDir() {
- File baseDir = new File(getDefaultSystemDir(), "netstats");
- baseDir.mkdirs();
- return baseDir;
- }
-
private static @NonNull Clock getDefaultClock() {
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
Clock.systemUTC());
@@ -506,8 +524,7 @@
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
alarmManager, wakeLock, getDefaultClock(),
new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
- new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(),
- new Dependencies());
+ new NetworkStatsObservers(), new Dependencies());
return service;
}
@@ -517,8 +534,8 @@
@VisibleForTesting
NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
- NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir,
- File baseDir, @NonNull Dependencies deps) {
+ NetworkStatsFactory factory, NetworkStatsObservers statsObservers,
+ @NonNull Dependencies deps) {
mContext = Objects.requireNonNull(context, "missing Context");
mNetd = Objects.requireNonNull(netd, "missing Netd");
mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
@@ -527,9 +544,11 @@
mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
mStatsFactory = Objects.requireNonNull(factory, "missing factory");
mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
- mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir");
- mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir");
mDeps = Objects.requireNonNull(deps, "missing Dependencies");
+ mStatsDir = mDeps.getOrCreateStatsDir();
+ if (!mStatsDir.exists()) {
+ throw new IllegalStateException("Persist data directory does not exist: " + mStatsDir);
+ }
final HandlerThread handlerThread = mDeps.makeHandlerThread();
handlerThread.start();
@@ -556,6 +575,87 @@
@VisibleForTesting
public static class Dependencies {
/**
+ * Get legacy platform stats directory.
+ */
+ @NonNull
+ public File getLegacyStatsDir() {
+ final File systemDataDir = new File(Environment.getDataDirectory(), "system");
+ return new File(systemDataDir, "netstats");
+ }
+
+ /**
+ * Get or create the directory that stores the persisted data usage.
+ */
+ @NonNull
+ public File getOrCreateStatsDir() {
+ final boolean storeInApexDataDir = getStoreFilesInApexData();
+
+ final File statsDataDir;
+ if (storeInApexDataDir) {
+ final File apexDataDir = ApexEnvironment
+ .getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ statsDataDir = new File(apexDataDir, "netstats");
+
+ } else {
+ statsDataDir = getLegacyStatsDir();
+ }
+
+ if (statsDataDir.exists() || statsDataDir.mkdirs()) {
+ return statsDataDir;
+ }
+ throw new IllegalStateException("Cannot write into stats data directory: "
+ + statsDataDir);
+ }
+
+ /**
+ * Get the count of import legacy target attempts.
+ */
+ public int getImportLegacyTargetAttempts() {
+ return DeviceConfigUtils.getDeviceConfigPropertyInt(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS,
+ DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS);
+ }
+
+ /**
+ * Create the persistent counter that counts total import legacy stats attempts.
+ */
+ public PersistentInt createImportLegacyAttemptsCounter(@NonNull Path path)
+ throws IOException {
+ // TODO: Modify PersistentInt to call setStartTime every time a write is made.
+ // Create and pass a real logger here.
+ return new PersistentInt(path.toString(), null /* logger */);
+ }
+
+ /**
+ * Create the persistent counter that counts total import legacy stats successes.
+ */
+ public PersistentInt createImportLegacySuccessesCounter(@NonNull Path path)
+ throws IOException {
+ return new PersistentInt(path.toString(), null /* logger */);
+ }
+
+ /**
+ * Get the flag of storing files in the apex data directory.
+ * @return whether to store files in the apex data directory.
+ */
+ public boolean getStoreFilesInApexData() {
+ return DeviceConfigUtils.getDeviceConfigPropertyBoolean(
+ DeviceConfig.NAMESPACE_TETHERING,
+ NETSTATS_STORE_FILES_IN_APEXDATA, true);
+ }
+
+ /**
+ * Read legacy persisted network stats from disk.
+ */
+ @NonNull
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) throws IOException {
+ return NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, bucketDuration);
+ }
+
+ /**
* Create a HandlerThread to use in NetworkStatsService.
*/
@NonNull
@@ -690,14 +790,15 @@
mSystemReady = true;
// create data recorders along with historical rotators
- mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false);
- mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false);
- mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false);
- mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true);
+ mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir);
+ mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir);
+ mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir);
+ mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
+ mStatsDir);
updatePersistThresholdsLocked();
- // upgrade any legacy stats, migrating them to rotated files
+ // upgrade any legacy stats
maybeUpgradeLegacyStatsLocked();
// read historical network stats from disk, since policy service
@@ -757,11 +858,12 @@
}
private NetworkStatsRecorder buildRecorder(
- String prefix, NetworkStatsSettings.Config config, boolean includeTags) {
+ String prefix, NetworkStatsSettings.Config config, boolean includeTags,
+ File baseDir) {
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
Context.DROPBOX_SERVICE);
return new NetworkStatsRecorder(new FileRotator(
- mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+ baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
}
@@ -791,32 +893,285 @@
mSystemReady = false;
}
+ private static class MigrationInfo {
+ public final NetworkStatsRecorder recorder;
+ public NetworkStatsCollection collection;
+ public boolean imported;
+ MigrationInfo(@NonNull final NetworkStatsRecorder recorder) {
+ this.recorder = recorder;
+ collection = null;
+ imported = false;
+ }
+ }
+
@GuardedBy("mStatsLock")
private void maybeUpgradeLegacyStatsLocked() {
- File file;
- try {
- file = new File(mSystemDir, "netstats.bin");
- if (file.exists()) {
- mDevRecorder.importLegacyNetworkLocked(file);
- file.delete();
- }
-
- file = new File(mSystemDir, "netstats_xt.bin");
- if (file.exists()) {
- file.delete();
- }
-
- file = new File(mSystemDir, "netstats_uid.bin");
- if (file.exists()) {
- mUidRecorder.importLegacyUidLocked(file);
- mUidTagRecorder.importLegacyUidLocked(file);
- file.delete();
- }
- } catch (IOException e) {
- Log.wtf(TAG, "problem during legacy upgrade", e);
- } catch (OutOfMemoryError e) {
- Log.wtf(TAG, "problem during legacy upgrade", e);
+ final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData();
+ if (!storeFilesInApexData) {
+ return;
}
+ try {
+ mImportLegacyAttemptsCounter = mDeps.createImportLegacyAttemptsCounter(
+ mStatsDir.toPath().resolve(NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME));
+ mImportLegacySuccessesCounter = mDeps.createImportLegacySuccessesCounter(
+ mStatsDir.toPath().resolve(NETSTATS_IMPORT_SUCCESS_COUNTER_NAME));
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
+ return;
+ }
+
+ final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
+ final int attempts;
+ try {
+ attempts = mImportLegacyAttemptsCounter.get();
+ } catch (IOException e) {
+ Log.wtf(TAG, "Failed to read attempts counter, skip.", e);
+ return;
+ }
+ if (attempts >= targetAttempts) return;
+
+ Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
+
+ final MigrationInfo[] migrations = new MigrationInfo[]{
+ new MigrationInfo(mDevRecorder), new MigrationInfo(mXtRecorder),
+ new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
+ };
+
+ // Legacy directories will be created by recorders if they do not exist
+ final File legacyBaseDir = mDeps.getLegacyStatsDir();
+ final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
+ buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir),
+ buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir),
+ buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir),
+ buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir)
+ };
+
+ long migrationEndTime = Long.MIN_VALUE;
+ boolean endedWithFallback = false;
+ try {
+ // First, read all legacy collections. This is OEM code and it can throw. Don't
+ // commit any data to disk until all are read.
+ for (int i = 0; i < migrations.length; i++) {
+ final MigrationInfo migration = migrations[i];
+ migration.collection = readPlatformCollectionForRecorder(migration.recorder);
+
+ // Also read the collection with legacy method
+ final NetworkStatsRecorder legacyRecorder = legacyRecorders[i];
+
+ final NetworkStatsCollection legacyStats;
+ try {
+ legacyStats = legacyRecorder.getOrLoadCompleteLocked();
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Failed to read stats with legacy method", e);
+ // Newer stats will be used here; that's the only thing that is usable
+ continue;
+ }
+
+ String errMsg;
+ Throwable exception = null;
+ try {
+ errMsg = compareStats(migration.collection, legacyStats);
+ } catch (Throwable e) {
+ errMsg = "Failed to compare migrated stats with all stats";
+ exception = e;
+ }
+
+ if (errMsg != null) {
+ Log.wtf(TAG, "NetworkStats import for migration " + i
+ + " returned invalid data: " + errMsg, exception);
+ // Fall back to legacy stats for this boot. The stats for old data will be
+ // re-imported again on next boot until they succeed the import. This is fine
+ // since every import clears the previous stats for the imported timespan.
+ migration.collection = legacyStats;
+ endedWithFallback = true;
+ }
+ }
+
+ // Find the latest end time.
+ for (final MigrationInfo migration : migrations) {
+ final long migrationEnd = migration.collection.getEndMillis();
+ if (migrationEnd > migrationEndTime) migrationEndTime = migrationEnd;
+ }
+
+ // Reading all collections from legacy data has succeeded. At this point it is
+ // safe to start overwriting the files on disk. The next step is to remove all
+ // data in the new location that overlaps with imported data. This ensures that
+ // any data in the new location that was created by a previous failed import is
+ // ignored. After that, write the imported data into the recorder. The code
+ // below can still possibly throw (disk error or OutOfMemory for example), but
+ // does not depend on code from non-mainline code.
+ Log.i(TAG, "Rewriting data with imported collections with cutoff "
+ + Instant.ofEpochMilli(migrationEndTime));
+ for (final MigrationInfo migration : migrations) {
+ migration.imported = true;
+ migration.recorder.removeDataBefore(migrationEndTime);
+ if (migration.collection.isEmpty()) continue;
+ migration.recorder.importCollectionLocked(migration.collection);
+ }
+
+ if (endedWithFallback) {
+ Log.wtf(TAG, "Imported platform collections with legacy fallback");
+ } else {
+ Log.i(TAG, "Successfully imported platform collections");
+ }
+ } catch (Throwable e) {
+ // The code above calls OEM code that may behave differently across devices.
+ // It can throw any exception including RuntimeExceptions and
+ // OutOfMemoryErrors. Try to recover anyway.
+ Log.wtf(TAG, "Platform data import failed. Remaining tries "
+ + (targetAttempts - attempts), e);
+
+ // Failed this time around : try again next time unless we're out of tries.
+ try {
+ mImportLegacyAttemptsCounter.set(attempts + 1);
+ } catch (IOException ex) {
+ Log.wtf(TAG, "Failed to update attempts counter.", ex);
+ }
+
+ // Try to remove any data from the failed import.
+ if (migrationEndTime > Long.MIN_VALUE) {
+ try {
+ for (final MigrationInfo migration : migrations) {
+ if (migration.imported) {
+ migration.recorder.removeDataBefore(migrationEndTime);
+ }
+ }
+ } catch (Throwable f) {
+ // If rollback still throws, there isn't much left to do. Try nuking
+ // all data, since that's the last stop. If nuking still throws, the
+ // framework will reboot, and if there are remaining tries, the migration
+ // process will retry, which is fine because it's idempotent.
+ for (final MigrationInfo migration : migrations) {
+ migration.recorder.recoverAndDeleteData();
+ }
+ }
+ }
+
+ return;
+ }
+
+ // Success ! No need to import again next time.
+ try {
+ mImportLegacyAttemptsCounter.set(targetAttempts);
+ // The successes counter is only for debugging. Hence, the synchronization
+ // between these two counters are not very critical.
+ final int successCount = mImportLegacySuccessesCounter.get();
+ mImportLegacySuccessesCounter.set(successCount + 1);
+ } catch (IOException e) {
+ Log.wtf(TAG, "Succeed but failed to update counters.", e);
+ }
+ }
+
+ private static String str(NetworkStatsCollection.Key key) {
+ StringBuilder sb = new StringBuilder()
+ .append(key.ident.toString())
+ .append(" uid=").append(key.uid);
+ if (key.set != SET_FOREGROUND) {
+ sb.append(" set=").append(key.set);
+ }
+ if (key.tag != 0) {
+ sb.append(" tag=").append(key.tag);
+ }
+ return sb.toString();
+ }
+
+ // The importer will modify some keys when importing them.
+ // In order to keep the comparison code simple, add such special cases here and simply
+ // ignore them. This should not impact fidelity much because the start/end checks and the total
+ // bytes check still need to pass.
+ private static boolean couldKeyChangeOnImport(NetworkStatsCollection.Key key) {
+ if (key.ident.isEmpty()) return false;
+ final NetworkIdentity firstIdent = key.ident.iterator().next();
+
+ // Non-mobile network with non-empty RAT type.
+ // This combination is invalid and the NetworkIdentity.Builder will throw if it is passed
+ // in, but it looks like it was previously possible to persist it to disk. The importer sets
+ // the RAT type to NETWORK_TYPE_ALL.
+ if (firstIdent.getType() != ConnectivityManager.TYPE_MOBILE
+ && firstIdent.getRatType() != NetworkTemplate.NETWORK_TYPE_ALL) {
+ return true;
+ }
+
+ return false;
+ }
+
+ @Nullable
+ private static String compareStats(
+ NetworkStatsCollection migrated, NetworkStatsCollection legacy) {
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> migEntries =
+ migrated.getEntries();
+ final Map<NetworkStatsCollection.Key, NetworkStatsHistory> legEntries = legacy.getEntries();
+
+ final ArraySet<NetworkStatsCollection.Key> unmatchedLegKeys =
+ new ArraySet<>(legEntries.keySet());
+
+ for (NetworkStatsCollection.Key legKey : legEntries.keySet()) {
+ final NetworkStatsHistory legHistory = legEntries.get(legKey);
+ final NetworkStatsHistory migHistory = migEntries.get(legKey);
+
+ if (migHistory == null && couldKeyChangeOnImport(legKey)) {
+ unmatchedLegKeys.remove(legKey);
+ continue;
+ }
+
+ if (migHistory == null) {
+ return "Missing migrated history for legacy key " + str(legKey)
+ + ", legacy history was " + legHistory;
+ }
+ if (!migHistory.isSameAs(legHistory)) {
+ return "Difference in history for key " + legKey + "; legacy history " + legHistory
+ + ", migrated history " + migHistory;
+ }
+ unmatchedLegKeys.remove(legKey);
+ }
+
+ if (!unmatchedLegKeys.isEmpty()) {
+ final NetworkStatsHistory first = legEntries.get(unmatchedLegKeys.valueAt(0));
+ return "Found unmatched legacy keys: count=" + unmatchedLegKeys.size()
+ + ", first unmatched collection " + first;
+ }
+
+ if (migrated.getStartMillis() != legacy.getStartMillis()
+ || migrated.getEndMillis() != legacy.getEndMillis()) {
+ return "Start / end of the collections "
+ + migrated.getStartMillis() + "/" + legacy.getStartMillis() + " and "
+ + migrated.getEndMillis() + "/" + legacy.getEndMillis()
+ + " don't match";
+ }
+
+ if (migrated.getTotalBytes() != legacy.getTotalBytes()) {
+ return "Total bytes " + migrated.getTotalBytes() + " and " + legacy.getTotalBytes()
+ + " don't match for collections with start/end "
+ + migrated.getStartMillis()
+ + "/" + legacy.getStartMillis();
+ }
+
+ return null;
+ }
+
+ @GuardedBy("mStatsLock")
+ @NonNull
+ private NetworkStatsCollection readPlatformCollectionForRecorder(
+ @NonNull final NetworkStatsRecorder rec) throws IOException {
+ final String prefix = rec.getCookie();
+ Log.i(TAG, "Importing platform collection for prefix " + prefix);
+ final NetworkStatsCollection collection = Objects.requireNonNull(
+ mDeps.readPlatformCollection(prefix, rec.getBucketDuration()),
+ "Imported platform collection for prefix " + prefix + " must not be null");
+
+ final long bootTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime();
+ if (!collection.isEmpty() && bootTimestamp < collection.getStartMillis()) {
+ throw new IllegalArgumentException("Platform collection for prefix " + prefix
+ + " contains data that could not possibly come from the previous boot "
+ + "(start timestamp = " + Instant.ofEpochMilli(collection.getStartMillis())
+ + ", last booted at " + Instant.ofEpochMilli(bootTimestamp));
+ }
+
+ Log.i(TAG, "Successfully read platform collection spanning from "
+ // Instant uses ISO-8601 for toString()
+ + Instant.ofEpochMilli(collection.getStartMillis()).toString() + " to "
+ + Instant.ofEpochMilli(collection.getEndMillis()).toString());
+ return collection;
}
/**
@@ -2102,10 +2457,32 @@
return;
}
+ pw.println("Directory:");
+ pw.increaseIndent();
+ pw.println(mStatsDir);
+ pw.decreaseIndent();
+
pw.println("Configs:");
pw.increaseIndent();
pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
pw.println();
+ pw.print(NETSTATS_STORE_FILES_IN_APEXDATA, mDeps.getStoreFilesInApexData());
+ pw.println();
+ pw.print(NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS, mDeps.getImportLegacyTargetAttempts());
+ pw.println();
+ if (mDeps.getStoreFilesInApexData()) {
+ try {
+ pw.print("platform legacy stats import attempts count",
+ mImportLegacyAttemptsCounter.get());
+ pw.println();
+ pw.print("platform legacy stats import successes count",
+ mImportLegacySuccessesCounter.get());
+ pw.println();
+ } catch (IOException e) {
+ pw.println("(failed to dump platform legacy stats import counters)");
+ }
+ }
+
pw.decreaseIndent();
pw.println("Active interfaces:");
diff --git a/service-t/src/com/android/server/net/PersistentInt.java b/service-t/src/com/android/server/net/PersistentInt.java
new file mode 100644
index 0000000..c212b77
--- /dev/null
+++ b/service-t/src/com/android/server/net/PersistentInt.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.AtomicFile;
+import android.util.SystemConfigFileCommitEventLogger;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+/**
+ * A simple integer backed by an on-disk {@link AtomicFile}. Not thread-safe.
+ */
+public class PersistentInt {
+ private final String mPath;
+ private final AtomicFile mFile;
+
+ /**
+ * Constructs a new {@code PersistentInt}. The counter is set to 0 if the file does not exist.
+ * Before returning, the constructor checks that the file is readable and writable. This
+ * indicates that in the future {@link #get} and {@link #set} are likely to succeed,
+ * though other events (data corruption, other code deleting the file, etc.) may cause these
+ * calls to fail in the future.
+ *
+ * @param path the path of the file to use.
+ * @param logger the logger
+ * @throws IOException the counter could not be read or written
+ */
+ public PersistentInt(@NonNull String path, @Nullable SystemConfigFileCommitEventLogger logger)
+ throws IOException {
+ mPath = path;
+ mFile = new AtomicFile(new File(path), logger);
+ checkReadWrite();
+ }
+
+ private void checkReadWrite() throws IOException {
+ int value;
+ try {
+ value = get();
+ } catch (FileNotFoundException e) {
+ // Counter does not exist. Attempt to initialize to 0.
+ // Note that we cannot tell here if the file does not exist or if opening it failed,
+ // because in Java both of those throw FileNotFoundException.
+ value = 0;
+ }
+ set(value);
+ get();
+ // No exceptions? Good.
+ }
+
+ /**
+ * Gets the current value.
+ *
+ * @return the current value of the counter.
+ * @throws IOException if reading the value failed.
+ */
+ public int get() throws IOException {
+ try (FileInputStream fin = mFile.openRead();
+ DataInputStream din = new DataInputStream(fin)) {
+ return din.readInt();
+ }
+ }
+
+ /**
+ * Sets the current value.
+ * @param value the value to set
+ * @throws IOException if writing the value failed.
+ */
+ public void set(int value) throws IOException {
+ FileOutputStream fout = null;
+ try {
+ fout = mFile.startWrite();
+ DataOutputStream dout = new DataOutputStream(fout);
+ dout.writeInt(value);
+ mFile.finishWrite(fout);
+ } catch (IOException e) {
+ if (fout != null) {
+ mFile.failWrite(fout);
+ }
+ throw e;
+ }
+ }
+
+ public String getPath() {
+ return mPath;
+ }
+}
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index a9ede6a..55db393 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -455,15 +455,15 @@
int TrafficController::toggleUidOwnerMap(ChildChain chain, bool enable) {
std::lock_guard guard(mMutex);
uint32_t key = UID_RULES_CONFIGURATION_KEY;
- auto oldConfiguration = mConfigurationMap.readValue(key);
- if (!oldConfiguration.ok()) {
+ auto oldConfigure = mConfigurationMap.readValue(key);
+ if (!oldConfigure.ok()) {
ALOGE("Cannot read the old configuration from map: %s",
- oldConfiguration.error().message().c_str());
- return -oldConfiguration.error().code();
+ oldConfigure.error().message().c_str());
+ return -oldConfigure.error().code();
}
Status res;
BpfConfig newConfiguration;
- uint8_t match;
+ uint32_t match;
switch (chain) {
case DOZABLE:
match = DOZABLE_MATCH;
@@ -484,7 +484,7 @@
return -EINVAL;
}
newConfiguration =
- enable ? (oldConfiguration.value() | match) : (oldConfiguration.value() & (~match));
+ enable ? (oldConfigure.value() | match) : (oldConfigure.value() & (~match));
res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
if (!isOk(res)) {
ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
@@ -496,17 +496,17 @@
std::lock_guard guard(mMutex);
uint32_t key = CURRENT_STATS_MAP_CONFIGURATION_KEY;
- auto oldConfiguration = mConfigurationMap.readValue(key);
- if (!oldConfiguration.ok()) {
+ auto oldConfigure = mConfigurationMap.readValue(key);
+ if (!oldConfigure.ok()) {
ALOGE("Cannot read the old configuration from map: %s",
- oldConfiguration.error().message().c_str());
- return Status(oldConfiguration.error().code(), oldConfiguration.error().message());
+ oldConfigure.error().message().c_str());
+ return Status(oldConfigure.error().code(), oldConfigure.error().message());
}
// Write to the configuration map to inform the kernel eBPF program to switch
// from using one map to the other. Use flag BPF_EXIST here since the map should
// be already populated in initMaps.
- uint8_t newConfigure = (oldConfiguration.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
+ uint32_t newConfigure = (oldConfigure.value() == SELECT_MAP_A) ? SELECT_MAP_B : SELECT_MAP_A;
auto res = mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, newConfigure,
BPF_EXIST);
if (!res.ok()) {
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index afb5568..5fb8ae0 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -65,7 +65,7 @@
BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
BpfMap<uint32_t, StatsValue> mFakeAppUidStatsMap;
BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
- BpfMap<uint32_t, uint8_t> mFakeConfigurationMap;
+ BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index 79e75ac..d3d52e2 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -155,7 +155,7 @@
* Userspace can do scraping and cleaning job on the other one depending on the
* current configs.
*/
- bpf::BpfMap<uint32_t, uint8_t> mConfigurationMap GUARDED_BY(mMutex);
+ bpf::BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
/*
* mUidOwnerMap: Store uids that are used for bandwidth control uid match.
diff --git a/service/native/libs/libclat/Android.bp b/service/native/libs/libclat/Android.bp
index 68e4dc4..54d40ac 100644
--- a/service/native/libs/libclat/Android.bp
+++ b/service/native/libs/libclat/Android.bp
@@ -35,7 +35,8 @@
cc_test {
name: "libclat_test",
defaults: ["netd_defaults"],
- test_suites: ["device-tests"],
+ test_suites: ["general-tests", "mts-tethering"],
+ test_config_template: ":net_native_test_config_template",
srcs: [
"clatutils_test.cpp",
],
@@ -49,5 +50,14 @@
"liblog",
"libnetutils",
],
+ compile_multilib: "both",
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
require_root: true,
}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index f760d3b..e55678c 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -3872,7 +3872,6 @@
}
final boolean wasValidated = nai.lastValidated;
- final boolean wasDefault = isDefaultNetwork(nai);
final boolean wasPartial = nai.partialConnectivity;
nai.partialConnectivity = ((testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0);
final boolean partialConnectivityChanged =
@@ -9252,7 +9251,18 @@
params.networkCapabilities = networkAgent.networkCapabilities;
params.linkProperties = new LinkProperties(networkAgent.linkProperties,
true /* parcelSensitiveFields */);
- networkAgent.networkMonitor().notifyNetworkConnected(params);
+ // isAtLeastT() is conservative here, as recent versions of NetworkStack support the
+ // newer callback even before T. However getInterfaceVersion is a synchronized binder
+ // call that would cause a Log.wtf to be emitted from the system_server process, and
+ // in the absence of a satisfactory, scalable solution which follows an easy/standard
+ // process to check the interface version, just use an SDK check. NetworkStack will
+ // always be new enough when running on T+.
+ if (SdkLevel.isAtLeastT()) {
+ networkAgent.networkMonitor().notifyNetworkConnected(params);
+ } else {
+ networkAgent.networkMonitor().notifyNetworkConnected(params.linkProperties,
+ params.networkCapabilities);
+ }
scheduleUnvalidatedPrompt(networkAgent);
// Whether a particular NetworkRequest listen should cause signal strength thresholds to
diff --git a/service/src/com/android/server/net/DelayedDiskWrite.java b/service/src/com/android/server/net/DelayedDiskWrite.java
index 35dc455..41cb419 100644
--- a/service/src/com/android/server/net/DelayedDiskWrite.java
+++ b/service/src/com/android/server/net/DelayedDiskWrite.java
@@ -21,6 +21,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
@@ -32,11 +34,42 @@
public class DelayedDiskWrite {
private static final String TAG = "DelayedDiskWrite";
+ private final Dependencies mDeps;
+
private HandlerThread mDiskWriteHandlerThread;
private Handler mDiskWriteHandler;
/* Tracks multiple writes on the same thread */
private int mWriteSequence = 0;
+ public DelayedDiskWrite() {
+ this(new Dependencies());
+ }
+
+ @VisibleForTesting
+ DelayedDiskWrite(Dependencies deps) {
+ mDeps = deps;
+ }
+
+ /**
+ * Dependencies class of DelayedDiskWrite, used for injection in tests.
+ */
+ @VisibleForTesting
+ static class Dependencies {
+ /**
+ * Create a HandlerThread to use in DelayedDiskWrite.
+ */
+ public HandlerThread makeHandlerThread() {
+ return new HandlerThread("DelayedDiskWriteThread");
+ }
+
+ /**
+ * Quit the HandlerThread looper.
+ */
+ public void quitHandlerThread(HandlerThread handlerThread) {
+ handlerThread.getLooper().quit();
+ }
+ }
+
/**
* Used to do a delayed data write to a given {@link OutputStream}.
*/
@@ -65,7 +98,7 @@
/* Do a delayed write to disk on a separate handler thread */
synchronized (this) {
if (++mWriteSequence == 1) {
- mDiskWriteHandlerThread = new HandlerThread("DelayedDiskWriteThread");
+ mDiskWriteHandlerThread = mDeps.makeHandlerThread();
mDiskWriteHandlerThread.start();
mDiskWriteHandler = new Handler(mDiskWriteHandlerThread.getLooper());
}
@@ -99,9 +132,9 @@
// Quit if no more writes sent
synchronized (this) {
if (--mWriteSequence == 0) {
- mDiskWriteHandler.getLooper().quit();
- mDiskWriteHandler = null;
+ mDeps.quitHandlerThread(mDiskWriteHandlerThread);
mDiskWriteHandlerThread = null;
+ mDiskWriteHandler = null;
}
}
}
diff --git a/tests/common/Android.bp b/tests/common/Android.bp
index 509e881..58731e0 100644
--- a/tests/common/Android.bp
+++ b/tests/common/Android.bp
@@ -140,6 +140,30 @@
],
}
+// defaults for tests that need to build against framework-connectivity's @hide APIs, but also
+// using fully @hide classes that are jarjared (because they have no API member). Similar to
+// framework-connectivity-test-defaults above but uses pre-jarjar class names.
+// Only usable from targets that have visibility on framework-connectivity-pre-jarjar, and apply
+// connectivity jarjar rules so that references to jarjared classes still match: this is limited to
+// connectivity internal tests only.
+java_defaults {
+ name: "framework-connectivity-internal-test-defaults",
+ sdk_version: "core_platform", // tests can use @CorePlatformApi's
+ libs: [
+ // order matters: classes in framework-connectivity are resolved before framework,
+ // meaning @hide APIs in framework-connectivity are resolved before @SystemApi
+ // stubs in framework
+ "framework-connectivity-pre-jarjar",
+ "framework-connectivity-t-pre-jarjar",
+ "framework-tethering.impl",
+ "framework",
+
+ // if sdk_version="" this gets automatically included, but here we need to add manually.
+ "framework-res",
+ ],
+ defaults_visibility: ["//packages/modules/Connectivity/tests:__subpackages__"],
+}
+
// Defaults for tests that want to run in mainline-presubmit.
// Not widely used because many of our tests have AndroidTest.xml files and
// use the mainline-param config-descriptor metadata in AndroidTest.xml.
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index b66a979..581ee22 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -46,6 +46,7 @@
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk31;
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -1307,6 +1308,7 @@
}
@Test @IgnoreUpTo(SC_V2)
+ @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden on T or above")
@DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
public void testExcludedRoutesDisabled() {
final LinkProperties lp = new LinkProperties();
diff --git a/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
index c2654c5..f8e041a 100644
--- a/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
+++ b/tests/common/java/android/net/netstats/NetworkStatsHistoryTest.kt
@@ -27,6 +27,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
@ConnectivityModuleTest
@RunWith(JUnit4::class)
@@ -51,12 +52,22 @@
.build()
statsSingle.assertEntriesEqual(entry1)
assertEquals(DateUtils.HOUR_IN_MILLIS, statsSingle.bucketDuration)
+
+ // Verify the builder throws if the timestamp of added entry is not greater than
+ // that of any previously-added entry.
+ assertFailsWith(IllegalArgumentException::class) {
+ NetworkStatsHistory
+ .Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
+ .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+ .build()
+ }
+
val statsMultiple = NetworkStatsHistory
.Builder(DateUtils.SECOND_IN_MILLIS, /* initialCapacity */ 0)
- .addEntry(entry1).addEntry(entry2).addEntry(entry3)
+ .addEntry(entry3).addEntry(entry1).addEntry(entry2)
.build()
assertEquals(DateUtils.SECOND_IN_MILLIS, statsMultiple.bucketDuration)
- statsMultiple.assertEntriesEqual(entry1, entry2, entry3)
+ statsMultiple.assertEntriesEqual(entry3, entry1, entry2)
}
fun NetworkStatsHistory.assertEntriesEqual(vararg entries: NetworkStatsHistory.Entry) {
diff --git a/tests/cts/net/Android.bp b/tests/cts/net/Android.bp
index 4d85d72..a6ed762 100644
--- a/tests/cts/net/Android.bp
+++ b/tests/cts/net/Android.bp
@@ -62,6 +62,7 @@
// sdk_version: "current",
platform_apis: true,
required: ["ConnectivityChecker"],
+ test_config_template: "AndroidTestTemplate.xml",
}
// Networking CTS tests for development and release. These tests always target the platform SDK
@@ -79,7 +80,16 @@
"cts",
"general-tests",
],
- test_config_template: "AndroidTestTemplate.xml",
+}
+
+java_defaults {
+ name: "CtsNetTestCasesApiStableDefaults",
+ // TODO: CTS should not depend on the entirety of the networkstack code.
+ static_libs: [
+ "NetworkStackApiStableLib",
+ ],
+ jni_uses_sdk_apis: true,
+ min_sdk_version: "29",
}
// Networking CTS tests that target the latest released SDK. These tests can be installed on release
@@ -87,13 +97,10 @@
// on release devices.
android_test {
name: "CtsNetTestCasesLatestSdk",
- defaults: ["CtsNetTestCasesDefaults"],
- // TODO: CTS should not depend on the entirety of the networkstack code.
- static_libs: [
- "NetworkStackApiStableLib",
+ defaults: [
+ "CtsNetTestCasesDefaults",
+ "CtsNetTestCasesApiStableDefaults",
],
- jni_uses_sdk_apis: true,
- min_sdk_version: "29",
target_sdk_version: "33",
test_suites: [
"general-tests",
@@ -102,5 +109,21 @@
"mts-tethering",
"mts-wifi",
],
- test_config_template: "AndroidTestTemplate.xml",
}
+
+android_test {
+ name: "CtsNetTestCasesMaxTargetSdk31", // Must match CtsNetTestCasesMaxTargetSdk31 annotation.
+ defaults: [
+ "CtsNetTestCasesDefaults",
+ "CtsNetTestCasesApiStableDefaults",
+ ],
+ target_sdk_version: "31",
+ package_name: "android.net.cts.maxtargetsdk31", // CTS package names must be unique.
+ instrumentation_target_package: "android.net.cts.maxtargetsdk31",
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts-networking",
+ ],
+}
+
diff --git a/tests/cts/net/AndroidTestTemplate.xml b/tests/cts/net/AndroidTestTemplate.xml
index 33f3af5..d2fb04a 100644
--- a/tests/cts/net/AndroidTestTemplate.xml
+++ b/tests/cts/net/AndroidTestTemplate.xml
@@ -33,10 +33,21 @@
<target_preparer class="com.android.testutils.DisableConfigSyncTargetPreparer">
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
- <option name="package" value="android.net.cts" />
+ <option name="package" value="{PACKAGE}" />
<option name="runtime-hint" value="9m4s" />
<option name="hidden-api-checks" value="false" />
<option name="isolated-storage" value="false" />
+ <!-- Test filter that allows test APKs to select which tests they want to run by annotating
+ those tests with an annotation matching the name of the APK.
+
+ This allows us to maintain one AndroidTestTemplate.xml for all CtsNetTestCases*.apk,
+ and have CtsNetTestCases and CtsNetTestCasesLatestSdk run all tests, but have
+ CtsNetTestCasesMaxTargetSdk31 run only tests that require target SDK 31.
+
+ This relies on the fact that if the class specified in include-annotation exists, then
+ the runner will only run the tests annotated with that annotation, but if it does not,
+ the runner will run all the tests. -->
+ <option name="include-annotation" value="com.android.testutils.filters.{MODULE}" />
</test>
<!-- When this test is run in a Mainline context (e.g. with `mts-tradefed`), only enable it if
one of the Mainline modules below is present on the device used for testing. -->
diff --git a/tests/cts/tethering/Android.bp b/tests/cts/tethering/Android.bp
index e9c4e5a..6096a8b 100644
--- a/tests/cts/tethering/Android.bp
+++ b/tests/cts/tethering/Android.bp
@@ -56,7 +56,7 @@
defaults: ["CtsTetheringTestDefaults"],
min_sdk_version: "30",
- target_sdk_version: "30",
+ target_sdk_version: "33",
static_libs: [
"TetheringIntegrationTestsLatestSdkLib",
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index e6cf22a..3ea27f7 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -87,7 +87,7 @@
name: "FrameworksNetTestsDefaults",
min_sdk_version: "30",
defaults: [
- "framework-connectivity-test-defaults",
+ "framework-connectivity-internal-test-defaults",
],
srcs: [
"java/**/*.java",
diff --git a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
index 743d39e..aa5a246 100644
--- a/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
+++ b/tests/unit/java/android/net/netstats/NetworkStatsDataMigrationUtilsTest.kt
@@ -61,14 +61,6 @@
assertValues(builder.build(), 55, 1814302L, 21050L, 31001636L, 26152L)
}
- @Test
- fun testMaybeReadLegacyUid() {
- val builder = NetworkStatsCollection.Builder(BUCKET_DURATION_MS)
- NetworkStatsDataMigrationUtils.readLegacyUid(builder,
- getInputStreamForResource(R.raw.netstats_uid_v4), false /* taggedData */)
- assertValues(builder.build(), 223, 106245210L, 710722L, 1130647496L, 1103989L)
- }
-
private fun assertValues(
collection: NetworkStatsCollection,
expectedSize: Int,
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index 0354377..64355ed 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -125,6 +125,7 @@
fullInfo.setPort(4242);
fullInfo.setHost(LOCALHOST);
fullInfo.setNetwork(new Network(123));
+ fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
NsdServiceInfo noHostInfo = new NsdServiceInfo();
@@ -175,6 +176,7 @@
assertEquals(original.getHost(), result.getHost());
assertTrue(original.getPort() == result.getPort());
assertEquals(original.getNetwork(), result.getNetwork());
+ assertEquals(original.getInterfaceIndex(), result.getInterfaceIndex());
// Assert equality of attribute map.
Map<String, byte[]> originalMap = original.getAttributes();
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index ef97168..1ea8d75 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -11704,6 +11704,12 @@
mCm.unregisterNetworkCallback(networkCallback);
}
+ private void verifyDump(String[] args) {
+ final StringWriter stringWriter = new StringWriter();
+ mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), args);
+ assertFalse(stringWriter.toString().isEmpty());
+ }
+
@Test
public void testDumpDoesNotCrash() {
mServiceContext.setPermission(DUMP, PERMISSION_GRANTED);
@@ -11716,11 +11722,26 @@
.addTransportType(TRANSPORT_WIFI).build();
mCm.registerNetworkCallback(genericRequest, genericNetworkCallback);
mCm.registerNetworkCallback(wifiRequest, wifiNetworkCallback);
- final StringWriter stringWriter = new StringWriter();
- mService.dump(new FileDescriptor(), new PrintWriter(stringWriter), new String[0]);
+ verifyDump(new String[0]);
- assertFalse(stringWriter.toString().isEmpty());
+ // Verify dump with arguments.
+ final String dumpPrio = "--dump-priority";
+ final String[] dumpArgs = {dumpPrio};
+ verifyDump(dumpArgs);
+
+ final String[] highDumpArgs = {dumpPrio, "HIGH"};
+ verifyDump(highDumpArgs);
+
+ final String[] normalDumpArgs = {dumpPrio, "NORMAL"};
+ verifyDump(normalDumpArgs);
+
+ // Invalid args should do dumpNormal w/o exception
+ final String[] unknownDumpArgs = {dumpPrio, "UNKNOWN"};
+ verifyDump(unknownDumpArgs);
+
+ final String[] invalidDumpArgs = {"UNKNOWN"};
+ verifyDump(invalidDumpArgs);
}
@Test
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 7ecf1f3..9365bee 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -19,7 +19,9 @@
import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.any;
@@ -37,6 +39,12 @@
import android.compat.testing.PlatformCompatChangeRule;
import android.content.ContentResolver;
import android.content.Context;
+import android.net.INetd;
+import android.net.InetAddresses;
+import android.net.mdns.aidl.DiscoveryInfo;
+import android.net.mdns.aidl.GetAddressInfo;
+import android.net.mdns.aidl.IMDnsEventListener;
+import android.net.mdns.aidl.ResolutionInfo;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
import android.net.nsd.MDnsManager;
@@ -64,6 +72,7 @@
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -280,6 +289,105 @@
verify(mMockMDnsM, never()).stopDaemon();
}
+ @Test
+ public void testDiscoverOnTetheringDownstream() throws Exception {
+ NsdService service = makeService();
+ NsdManager client = connectClient(service);
+
+ final String serviceType = "a_type";
+ final String serviceName = "a_name";
+ final String domainName = "mytestdevice.local";
+ final int interfaceIdx = 123;
+ final NsdManager.DiscoveryListener discListener = mock(NsdManager.DiscoveryListener.class);
+ client.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discListener);
+ waitForIdle();
+
+ final ArgumentCaptor<IMDnsEventListener> listenerCaptor =
+ ArgumentCaptor.forClass(IMDnsEventListener.class);
+ verify(mMockMDnsM).registerEventListener(listenerCaptor.capture());
+ final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(serviceType),
+ eq(0) /* interfaceIdx */);
+ // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
+ // this needs to use a timeout
+ verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(serviceType);
+
+ final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
+ discIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_FOUND,
+ serviceName,
+ serviceType,
+ domainName,
+ interfaceIdx,
+ INetd.LOCAL_NET_ID); // LOCAL_NET_ID (99) used on tethering downstreams
+ final IMDnsEventListener eventListener = listenerCaptor.getValue();
+ eventListener.onServiceDiscoveryStatus(discoveryInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<NsdServiceInfo> discoveredInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ verify(discListener, timeout(TIMEOUT_MS)).onServiceFound(discoveredInfoCaptor.capture());
+ final NsdServiceInfo foundInfo = discoveredInfoCaptor.getValue();
+ assertEquals(serviceName, foundInfo.getServiceName());
+ assertEquals(serviceType, foundInfo.getServiceType());
+ assertNull(foundInfo.getHost());
+ assertNull(foundInfo.getNetwork());
+ assertEquals(interfaceIdx, foundInfo.getInterfaceIndex());
+
+ // After discovering the service, verify resolving it
+ final NsdManager.ResolveListener resolveListener = mock(NsdManager.ResolveListener.class);
+ client.resolveService(foundInfo, resolveListener);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(serviceName), eq(serviceType),
+ eq("local.") /* domain */, eq(interfaceIdx));
+
+ final int servicePort = 10123;
+ final String serviceFullName = serviceName + "." + serviceType;
+ final ResolutionInfo resolutionInfo = new ResolutionInfo(
+ resolvIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_RESOLVED,
+ null /* serviceName */,
+ null /* serviceType */,
+ null /* domain */,
+ serviceFullName,
+ domainName,
+ servicePort,
+ new byte[0] /* txtRecord */,
+ interfaceIdx);
+
+ doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+ eventListener.onServiceResolutionStatus(resolutionInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(domainName),
+ eq(interfaceIdx));
+
+ final String serviceAddress = "192.0.2.123";
+ final GetAddressInfo addressInfo = new GetAddressInfo(
+ getAddrIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+ serviceFullName,
+ serviceAddress,
+ interfaceIdx,
+ INetd.LOCAL_NET_ID);
+ eventListener.onGettingServiceAddressStatus(addressInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<NsdServiceInfo> resInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ verify(resolveListener, timeout(TIMEOUT_MS)).onServiceResolved(resInfoCaptor.capture());
+ final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
+ assertEquals(serviceName, resolvedService.getServiceName());
+ assertEquals("." + serviceType, resolvedService.getServiceType());
+ assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
+ assertEquals(servicePort, resolvedService.getPort());
+ assertNull(resolvedService.getNetwork());
+ assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
+ }
+
private void waitForIdle() {
HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
}
diff --git a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
index 1801c45..4adc999 100644
--- a/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
+++ b/tests/unit/java/com/android/server/net/IpConfigStoreTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
+import android.content.Context;
import android.net.InetAddresses;
import android.net.IpConfiguration;
import android.net.IpConfiguration.IpAssignment;
@@ -28,10 +29,14 @@
import android.net.ProxyInfo;
import android.net.StaticIpConfiguration;
import android.os.Build;
+import android.os.HandlerThread;
import android.util.ArrayMap;
+import androidx.test.InstrumentationRegistry;
+
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.HandlerUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,11 +44,13 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
/**
* Unit tests for {@link IpConfigStore}
@@ -51,6 +58,7 @@
@RunWith(DevSdkIgnoreRunner.class)
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class IpConfigStoreTest {
+ private static final int TIMEOUT_MS = 2_000;
private static final int KEY_CONFIG = 17;
private static final String IFACE_1 = "eth0";
private static final String IFACE_2 = "eth1";
@@ -59,6 +67,22 @@
private static final String DNS_IP_ADDR_1 = "1.2.3.4";
private static final String DNS_IP_ADDR_2 = "5.6.7.8";
+ private static final ArrayList<InetAddress> DNS_SERVERS = new ArrayList<>(List.of(
+ InetAddresses.parseNumericAddress(DNS_IP_ADDR_1),
+ InetAddresses.parseNumericAddress(DNS_IP_ADDR_2)));
+ private static final StaticIpConfiguration STATIC_IP_CONFIG_1 =
+ new StaticIpConfiguration.Builder()
+ .setIpAddress(new LinkAddress(IP_ADDR_1))
+ .setDnsServers(DNS_SERVERS)
+ .build();
+ private static final StaticIpConfiguration STATIC_IP_CONFIG_2 =
+ new StaticIpConfiguration.Builder()
+ .setIpAddress(new LinkAddress(IP_ADDR_2))
+ .setDnsServers(DNS_SERVERS)
+ .build();
+ private static final ProxyInfo PROXY_INFO =
+ ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
+
@Test
public void backwardCompatibility2to3() throws IOException {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
@@ -82,40 +106,73 @@
@Test
public void staticIpMultiNetworks() throws Exception {
- final ArrayList<InetAddress> dnsServers = new ArrayList<>();
- dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_1));
- dnsServers.add(InetAddresses.parseNumericAddress(DNS_IP_ADDR_2));
- final StaticIpConfiguration staticIpConfiguration1 = new StaticIpConfiguration.Builder()
- .setIpAddress(new LinkAddress(IP_ADDR_1))
- .setDnsServers(dnsServers).build();
- final StaticIpConfiguration staticIpConfiguration2 = new StaticIpConfiguration.Builder()
- .setIpAddress(new LinkAddress(IP_ADDR_2))
- .setDnsServers(dnsServers).build();
+ final IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
+ ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+ final IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
+ ProxySettings.STATIC, STATIC_IP_CONFIG_2, PROXY_INFO);
- ProxyInfo proxyInfo =
- ProxyInfo.buildDirectProxy("10.10.10.10", 88, Arrays.asList("host1", "host2"));
-
- IpConfiguration expectedConfig1 = newIpConfiguration(IpAssignment.STATIC,
- ProxySettings.STATIC, staticIpConfiguration1, proxyInfo);
- IpConfiguration expectedConfig2 = newIpConfiguration(IpAssignment.STATIC,
- ProxySettings.STATIC, staticIpConfiguration2, proxyInfo);
-
- ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+ final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
expectedNetworks.put(IFACE_1, expectedConfig1);
expectedNetworks.put(IFACE_2, expectedConfig2);
- MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
- IpConfigStore store = new IpConfigStore(writer);
+ final MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
+ final IpConfigStore store = new IpConfigStore(writer);
store.writeIpConfigurations("file/path/not/used/", expectedNetworks);
- InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
- ArrayMap<String, IpConfiguration> actualNetworks = IpConfigStore.readIpConfigurations(in);
+ final InputStream in = new ByteArrayInputStream(writer.mByteStream.toByteArray());
+ final ArrayMap<String, IpConfiguration> actualNetworks =
+ IpConfigStore.readIpConfigurations(in);
assertNotNull(actualNetworks);
assertEquals(2, actualNetworks.size());
assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
assertEquals(expectedNetworks.get(IFACE_2), actualNetworks.get(IFACE_2));
}
+ @Test
+ public void readIpConfigurationFromFilePath() throws Exception {
+ final HandlerThread testHandlerThread = new HandlerThread("IpConfigStoreTest");
+ final DelayedDiskWrite.Dependencies dependencies =
+ new DelayedDiskWrite.Dependencies() {
+ @Override
+ public HandlerThread makeHandlerThread() {
+ return testHandlerThread;
+ }
+ @Override
+ public void quitHandlerThread(HandlerThread handlerThread) {
+ testHandlerThread.quitSafely();
+ }
+ };
+
+ final IpConfiguration ipConfig = newIpConfiguration(IpAssignment.STATIC,
+ ProxySettings.STATIC, STATIC_IP_CONFIG_1, PROXY_INFO);
+ final ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+ expectedNetworks.put(IFACE_1, ipConfig);
+
+ // Write IP config to specific file path and read it later.
+ final Context context = InstrumentationRegistry.getContext();
+ final File configFile = new File(context.getFilesDir().getPath(),
+ "IpConfigStoreTest-ipconfig.txt");
+ final DelayedDiskWrite writer = new DelayedDiskWrite(dependencies);
+ final IpConfigStore store = new IpConfigStore(writer);
+ store.writeIpConfigurations(configFile.getPath(), expectedNetworks);
+ HandlerUtils.waitForIdle(testHandlerThread, TIMEOUT_MS);
+
+ // Read IP config from the file path.
+ final ArrayMap<String, IpConfiguration> actualNetworks =
+ IpConfigStore.readIpConfigurations(configFile.getPath());
+ assertNotNull(actualNetworks);
+ assertEquals(1, actualNetworks.size());
+ assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
+
+ // Return an empty array when reading IP configuration from an non-exist config file.
+ final ArrayMap<String, IpConfiguration> emptyNetworks =
+ IpConfigStore.readIpConfigurations("/dev/null");
+ assertNotNull(emptyNetworks);
+ assertEquals(0, emptyNetworks.size());
+
+ configFile.delete();
+ }
+
private IpConfiguration newIpConfiguration(IpAssignment ipAssignment,
ProxySettings proxySettings, StaticIpConfiguration staticIpConfig, ProxyInfo info) {
final IpConfiguration config = new IpConfiguration();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index ceeb997..f1820b3 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.app.usage.NetworkStatsManager.PREFIX_DEV;
import static android.content.Intent.ACTION_UID_REMOVED;
import static android.content.Intent.EXTRA_UID;
import static android.content.pm.PackageManager.PERMISSION_DENIED;
@@ -56,6 +57,9 @@
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.net.TrafficStats.UID_REMOVED;
import static android.net.TrafficStats.UID_TETHERING;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_UID_TAG;
+import static android.net.netstats.NetworkStatsDataMigrationUtils.PREFIX_XT;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
@@ -77,6 +81,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
@@ -96,6 +101,7 @@
import android.net.NetworkCapabilities;
import android.net.NetworkStateSnapshot;
import android.net.NetworkStats;
+import android.net.NetworkStatsCollection;
import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate;
import android.net.TelephonyNetworkSpecifier;
@@ -104,6 +110,7 @@
import android.net.UnderlyingNetworkInfo;
import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.wifi.WifiInfo;
+import android.os.DropBoxManager;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -112,11 +119,13 @@
import android.provider.Settings;
import android.system.ErrnoException;
import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
import androidx.annotation.Nullable;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
+import com.android.internal.util.FileRotator;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.net.module.util.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker;
@@ -131,6 +140,16 @@
import com.android.testutils.TestBpfMap;
import com.android.testutils.TestableNetworkStatsProviderBinder;
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
import libcore.testing.io.TestIoUtils;
import org.junit.After;
@@ -142,13 +161,6 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.io.File;
-import java.time.Clock;
-import java.time.ZoneOffset;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
-
/**
* Tests for {@link NetworkStatsService}.
*
@@ -187,6 +199,7 @@
private long mElapsedRealtime;
private File mStatsDir;
+ private File mLegacyStatsDir;
private MockContext mServiceContext;
private @Mock TelephonyManager mTelephonyManager;
private static @Mock WifiInfo sWifiInfo;
@@ -220,6 +233,12 @@
private ContentObserver mContentObserver;
private Handler mHandler;
private TetheringManager.TetheringEventCallback mTetheringEventCallback;
+ private Map<String, NetworkStatsCollection> mPlatformNetworkStatsCollection =
+ new ArrayMap<String, NetworkStatsCollection>();
+ private boolean mStoreFilesInApexData = false;
+ private int mImportLegacyTargetAttempts = 0;
+ private @Mock PersistentInt mImportLegacyAttemptsCounter;
+ private @Mock PersistentInt mImportLegacySuccessesCounter;
private class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext;
@@ -286,6 +305,8 @@
any(), any(), anyInt(), anyBoolean(), any())).thenReturn(true);
when(sWifiInfo.getNetworkKey()).thenReturn(TEST_WIFI_NETWORK_KEY);
mStatsDir = TestIoUtils.createTemporaryDirectory(getClass().getSimpleName());
+ mLegacyStatsDir = TestIoUtils.createTemporaryDirectory(
+ getClass().getSimpleName() + "-legacy");
PowerManager powerManager = (PowerManager) mServiceContext.getSystemService(
Context.POWER_SERVICE);
@@ -295,8 +316,7 @@
mHandlerThread = new HandlerThread("HandlerThread");
final NetworkStatsService.Dependencies deps = makeDependencies();
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
- mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
- getBaseDir(mStatsDir), deps);
+ mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
mElapsedRealtime = 0L;
@@ -339,6 +359,44 @@
private NetworkStatsService.Dependencies makeDependencies() {
return new NetworkStatsService.Dependencies() {
@Override
+ public File getLegacyStatsDir() {
+ return mLegacyStatsDir;
+ }
+
+ @Override
+ public File getOrCreateStatsDir() {
+ return mStatsDir;
+ }
+
+ @Override
+ public boolean getStoreFilesInApexData() {
+ return mStoreFilesInApexData;
+ }
+
+ @Override
+ public int getImportLegacyTargetAttempts() {
+ return mImportLegacyTargetAttempts;
+ }
+
+ @Override
+ public PersistentInt createImportLegacyAttemptsCounter(
+ @androidx.annotation.NonNull Path path) {
+ return mImportLegacyAttemptsCounter;
+ }
+
+ @Override
+ public PersistentInt createImportLegacySuccessesCounter(
+ @androidx.annotation.NonNull Path path) {
+ return mImportLegacySuccessesCounter;
+ }
+
+ @Override
+ public NetworkStatsCollection readPlatformCollection(
+ @NonNull String prefix, long bucketDuration) {
+ return mPlatformNetworkStatsCollection.get(prefix);
+ }
+
+ @Override
public HandlerThread makeHandlerThread() {
return mHandlerThread;
}
@@ -1704,10 +1762,108 @@
assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
}
- private static File getBaseDir(File statsDir) {
- File baseDir = new File(statsDir, "netstats");
- baseDir.mkdirs();
- return baseDir;
+ /**
+ * Verify the service will perform data migration process can be controlled by the device flag.
+ */
+ @Test
+ public void testDataMigration() throws Exception {
+ assertStatsFilesExist(false);
+ expectDefaultSettings();
+
+ NetworkStateSnapshot[] states = new NetworkStateSnapshot[] {buildWifiState()};
+
+ mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+ new UnderlyingNetworkInfo[0]);
+
+ // modify some number on wifi, and trigger poll event
+ incrementCurrentTime(HOUR_IN_MILLIS);
+ // expectDefaultSettings();
+ expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+ .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+ expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 2)
+ .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+ .insertEntry(TEST_IFACE, UID_RED, SET_DEFAULT, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+ .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, TAG_NONE, 512L, 4L, 256L, 2L, 0L)
+ .insertEntry(TEST_IFACE, UID_RED, SET_FOREGROUND, 0xFAAD, 256L, 2L, 128L, 1L, 0L)
+ .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+
+ mService.noteUidForeground(UID_RED, false);
+ verify(mUidCounterSetMap, never()).deleteEntry(any());
+ mService.incrementOperationCount(UID_RED, 0xFAAD, 4);
+ mService.noteUidForeground(UID_RED, true);
+ verify(mUidCounterSetMap).updateEntry(
+ eq(new U32(UID_RED)), eq(new U8((short) SET_FOREGROUND)));
+ mService.incrementOperationCount(UID_RED, 0xFAAD, 6);
+
+ forcePollAndWaitForIdle();
+ // Simulate shutdown to force persisting data
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(true);
+
+ // Move the files to the legacy directory to simulate an import from old data
+ for (File f : mStatsDir.listFiles()) {
+ Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+ }
+ assertStatsFilesExist(false);
+
+ // Fetch the stats from the legacy files and set platform stats collection to be identical
+ mPlatformNetworkStatsCollection.put(PREFIX_DEV,
+ getLegacyCollection(PREFIX_DEV, false /* includeTags */));
+ mPlatformNetworkStatsCollection.put(PREFIX_XT,
+ getLegacyCollection(PREFIX_XT, false /* includeTags */));
+ mPlatformNetworkStatsCollection.put(PREFIX_UID,
+ getLegacyCollection(PREFIX_UID, false /* includeTags */));
+ mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+ getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+ // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+ assertStatsFilesExist(false);
+
+ // Set the flag and reboot, verify the imported data is not there until next boot.
+ mStoreFilesInApexData = true;
+ mImportLegacyTargetAttempts = 3;
+ mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+ assertStatsFilesExist(false);
+
+ // Boot through systemReady() again.
+ expectDefaultSettings();
+ expectNetworkStatsUidDetail(buildEmptyStats());
+ expectSystemReady();
+ mService.systemReady();
+
+ // After systemReady(), the service should have historical stats loaded again.
+ // Thus, verify
+ // 1. The stats are absorbed by the recorder.
+ // 2. The imported data are persisted.
+ // 3. The attempts count is set to target attempts count to indicate a successful
+ // migration.
+ assertNetworkTotal(sTemplateWifi, 1024L, 8L, 2048L, 16L, 0);
+ assertStatsFilesExist(true);
+ verify(mImportLegacyAttemptsCounter).set(3);
+ verify(mImportLegacySuccessesCounter).set(1);
+
+ // TODO: Verify upgrading with Exception won't damege original data and
+ // will decrease the retry counter by 1.
+ }
+
+ private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
+ boolean includeTags) {
+ final NetworkStats.NonMonotonicObserver observer =
+ mock(NetworkStats.NonMonotonicObserver.class);
+ final DropBoxManager dropBox = mock(DropBoxManager.class);
+ return new NetworkStatsRecorder(new FileRotator(
+ directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+ observer, dropBox, prefix, config.bucketDuration, includeTags);
+ }
+
+ private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
+ final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, PREFIX_DEV,
+ mSettings.getDevConfig(), includeTags);
+ return recorder.getOrLoadCompleteLocked();
}
private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
@@ -1816,11 +1972,10 @@
}
private void assertStatsFilesExist(boolean exist) {
- final File basePath = new File(mStatsDir, "netstats");
if (exist) {
- assertTrue(basePath.list().length > 0);
+ assertTrue(mStatsDir.list().length > 0);
} else {
- assertTrue(basePath.list().length == 0);
+ assertTrue(mStatsDir.list().length == 0);
}
}
diff --git a/tests/unit/java/com/android/server/net/PersistentIntTest.kt b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
new file mode 100644
index 0000000..9268352
--- /dev/null
+++ b/tests/unit/java/com/android/server/net/PersistentIntTest.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net
+
+import android.util.SystemConfigFileCommitEventLogger
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.SC_V2
+import com.android.testutils.assertThrows
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.attribute.PosixFilePermission
+import java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE
+import java.nio.file.attribute.PosixFilePermission.OWNER_READ
+import java.nio.file.attribute.PosixFilePermission.OWNER_WRITE
+import java.util.Random
+import kotlin.test.assertEquals
+
+@RunWith(DevSdkIgnoreRunner::class)
+@IgnoreUpTo(SC_V2)
+class PersistentIntTest {
+ val tempFilesCreated = mutableSetOf<Path>()
+ lateinit var tempDir: Path
+
+ @Before
+ fun setUp() {
+ tempDir = Files.createTempDirectory("tmp.PersistentIntTest.")
+ }
+
+ @After
+ fun tearDown() {
+ var permissions = setOf(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE)
+ Files.setPosixFilePermissions(tempDir, permissions)
+
+ for (file in tempFilesCreated) {
+ Files.deleteIfExists(file)
+ }
+ Files.delete(tempDir)
+ }
+
+ @Test
+ fun testNormalReadWrite() {
+ // New, initialized to 0.
+ val pi = createPersistentInt()
+ assertEquals(0, pi.get())
+ pi.set(12345)
+ assertEquals(12345, pi.get())
+
+ // Existing.
+ val pi2 = createPersistentInt(pathOf(pi))
+ assertEquals(12345, pi2.get())
+ }
+
+ @Test
+ fun testReadOrWriteFailsInCreate() {
+ setWritable(tempDir, false)
+ assertThrows(IOException::class.java) {
+ createPersistentInt()
+ }
+ }
+
+ @Test
+ fun testReadOrWriteFailsAfterCreate() {
+ val pi = createPersistentInt()
+ pi.set(42)
+ assertEquals(42, pi.get())
+
+ val path = pathOf(pi)
+ setReadable(path, false)
+ assertThrows(IOException::class.java) { pi.get() }
+ pi.set(77)
+
+ setReadable(path, true)
+ setWritable(path, false)
+ setWritable(tempDir, false) // Writing creates a new file+renames, make this fail.
+ assertThrows(IOException::class.java) { pi.set(99) }
+ assertEquals(77, pi.get())
+ }
+
+ fun addOrRemovePermission(p: Path, permission: PosixFilePermission, add: Boolean) {
+ val permissions = Files.getPosixFilePermissions(p)
+ if (add) {
+ permissions.add(permission)
+ } else {
+ permissions.remove(permission)
+ }
+ Files.setPosixFilePermissions(p, permissions)
+ }
+
+ fun setReadable(p: Path, readable: Boolean) {
+ addOrRemovePermission(p, OWNER_READ, readable)
+ }
+
+ fun setWritable(p: Path, writable: Boolean) {
+ addOrRemovePermission(p, OWNER_WRITE, writable)
+ }
+
+ fun pathOf(pi: PersistentInt): Path {
+ return File(pi.path).toPath()
+ }
+
+ fun createPersistentInt(path: Path = randomTempPath()): PersistentInt {
+ tempFilesCreated.add(path)
+ return PersistentInt(path.toString(),
+ SystemConfigFileCommitEventLogger("PersistentIntTest"))
+ }
+
+ fun randomTempPath(): Path {
+ return tempDir.resolve(Integer.toHexString(Random().nextInt())).also {
+ tempFilesCreated.add(it)
+ }
+ }
+}