[MS08] Move NetworkStats files to f/b/package/ConnectivityT
NetworkStatsService is going to be moved into ConnectivityService
module. Move all related files to packages/ConnectivityT so that
it can be easily migrate these files to connectivity module
after clearing the hidden API usages.
Bug: 197717846
Test: TH
Change-Id: Iead00832b5eb7b1dc40a92027c5a14ae8316b16c
diff --git a/service-t/Sources.bp b/service-t/Sources.bp
index 529f58d..6a64910 100644
--- a/service-t/Sources.bp
+++ b/service-t/Sources.bp
@@ -19,13 +19,45 @@
default_applicable_licenses: ["Android-Apache-2.0"],
}
+// NetworkStats related libraries.
+
+filegroup {
+ name: "services.connectivity-netstats-sources",
+ srcs: [
+ "src/com/android/server/net/NetworkIdentity*.java",
+ "src/com/android/server/net/NetworkStats*.java",
+ ],
+ path: "src",
+ visibility: [
+ "//visibility:private",
+ ],
+}
+
+// Nsd related libraries.
+
filegroup {
name: "services.connectivity-nsd-sources",
srcs: [
- "src/**/*.java",
+ "src/com/android/server/INativeDaemon*.java",
+ "src/com/android/server/NativeDaemon*.java",
+ "src/com/android/server/Nsd*.java",
+ ],
+ path: "src",
+ visibility: [
+ "//visibility:private",
+ ],
+}
+
+// Connectivity-T common libraries.
+
+filegroup {
+ name: "services.connectivity-tiramisu-sources",
+ srcs: [
+ ":services.connectivity-netstats-sources",
+ ":services.connectivity-nsd-sources",
],
path: "src",
visibility: [
"//frameworks/base/services/core",
],
-}
+}
\ No newline at end of file
diff --git a/service-t/src/com/android/server/net/NetworkIdentitySet.java b/service-t/src/com/android/server/net/NetworkIdentitySet.java
new file mode 100644
index 0000000..22ed781
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkIdentitySet.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2011 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.net.NetworkIdentity;
+import android.service.NetworkIdentitySetProto;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.util.HashSet;
+
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+
+/**
+ * Identity of a {@code iface}, defined by the set of {@link NetworkIdentity}
+ * active on that interface.
+ *
+ * @hide
+ */
+public class NetworkIdentitySet extends HashSet<NetworkIdentity> implements
+ Comparable<NetworkIdentitySet> {
+ private static final int VERSION_INIT = 1;
+ private static final int VERSION_ADD_ROAMING = 2;
+ private static final int VERSION_ADD_NETWORK_ID = 3;
+ private static final int VERSION_ADD_METERED = 4;
+ private static final int VERSION_ADD_DEFAULT_NETWORK = 5;
+ private static final int VERSION_ADD_OEM_MANAGED_NETWORK = 6;
+
+ public NetworkIdentitySet() {
+ }
+
+ public NetworkIdentitySet(DataInput in) throws IOException {
+ final int version = in.readInt();
+ final int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ if (version <= VERSION_INIT) {
+ final int ignored = in.readInt();
+ }
+ final int type = in.readInt();
+ final int subType = in.readInt();
+ final String subscriberId = readOptionalString(in);
+ final String networkId;
+ if (version >= VERSION_ADD_NETWORK_ID) {
+ networkId = readOptionalString(in);
+ } else {
+ networkId = null;
+ }
+ final boolean roaming;
+ if (version >= VERSION_ADD_ROAMING) {
+ roaming = in.readBoolean();
+ } else {
+ roaming = false;
+ }
+
+ final boolean metered;
+ if (version >= VERSION_ADD_METERED) {
+ metered = in.readBoolean();
+ } else {
+ // If this is the old data and the type is mobile, treat it as metered. (Note that
+ // if this is a mobile network, TYPE_MOBILE is the only possible type that could be
+ // used.)
+ metered = (type == TYPE_MOBILE);
+ }
+
+ final boolean defaultNetwork;
+ if (version >= VERSION_ADD_DEFAULT_NETWORK) {
+ defaultNetwork = in.readBoolean();
+ } else {
+ defaultNetwork = true;
+ }
+
+ final int oemNetCapabilities;
+ if (version >= VERSION_ADD_OEM_MANAGED_NETWORK) {
+ oemNetCapabilities = in.readInt();
+ } else {
+ oemNetCapabilities = NetworkIdentity.OEM_NONE;
+ }
+
+ add(new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered,
+ defaultNetwork, oemNetCapabilities));
+ }
+ }
+
+ public void writeToStream(DataOutput out) throws IOException {
+ out.writeInt(VERSION_ADD_OEM_MANAGED_NETWORK);
+ out.writeInt(size());
+ for (NetworkIdentity ident : this) {
+ out.writeInt(ident.getType());
+ out.writeInt(ident.getSubType());
+ writeOptionalString(out, ident.getSubscriberId());
+ writeOptionalString(out, ident.getNetworkId());
+ out.writeBoolean(ident.getRoaming());
+ out.writeBoolean(ident.getMetered());
+ out.writeBoolean(ident.getDefaultNetwork());
+ out.writeInt(ident.getOemManaged());
+ }
+ }
+
+ /** @return whether any {@link NetworkIdentity} in this set is considered metered. */
+ public boolean isAnyMemberMetered() {
+ if (isEmpty()) {
+ return false;
+ }
+ for (NetworkIdentity ident : this) {
+ if (ident.getMetered()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** @return whether any {@link NetworkIdentity} in this set is considered roaming. */
+ public boolean isAnyMemberRoaming() {
+ if (isEmpty()) {
+ return false;
+ }
+ for (NetworkIdentity ident : this) {
+ if (ident.getRoaming()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** @return whether any {@link NetworkIdentity} in this set is considered on the default
+ network. */
+ public boolean areAllMembersOnDefaultNetwork() {
+ if (isEmpty()) {
+ return true;
+ }
+ for (NetworkIdentity ident : this) {
+ if (!ident.getDefaultNetwork()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void writeOptionalString(DataOutput out, String value) throws IOException {
+ if (value != null) {
+ out.writeByte(1);
+ out.writeUTF(value);
+ } else {
+ out.writeByte(0);
+ }
+ }
+
+ private static String readOptionalString(DataInput in) throws IOException {
+ if (in.readByte() != 0) {
+ return in.readUTF();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int compareTo(NetworkIdentitySet another) {
+ if (isEmpty()) return -1;
+ if (another.isEmpty()) return 1;
+
+ final NetworkIdentity ident = iterator().next();
+ final NetworkIdentity anotherIdent = another.iterator().next();
+ return ident.compareTo(anotherIdent);
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long tag) {
+ final long start = proto.start(tag);
+
+ for (NetworkIdentity ident : this) {
+ ident.dumpDebug(proto, NetworkIdentitySetProto.IDENTITIES);
+ }
+
+ proto.end(start);
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsAccess.java b/service-t/src/com/android/server/net/NetworkStatsAccess.java
new file mode 100644
index 0000000..d25eae4
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsAccess.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2015 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 static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_REMOVED;
+import static android.net.TrafficStats.UID_TETHERING;
+
+import android.Manifest;
+import android.annotation.IntDef;
+import android.app.AppOpsManager;
+import android.app.admin.DevicePolicyManagerInternal;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Process;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+
+import com.android.server.LocalServices;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Utility methods for controlling access to network stats APIs. */
+public final class NetworkStatsAccess {
+ private NetworkStatsAccess() {}
+
+ /**
+ * Represents an access level for the network usage history and statistics APIs.
+ *
+ * <p>Access levels are in increasing order; that is, it is reasonable to check access by
+ * verifying that the caller's access level is at least the minimum required level.
+ */
+ @IntDef({
+ Level.DEFAULT,
+ Level.USER,
+ Level.DEVICESUMMARY,
+ Level.DEVICE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Level {
+ /**
+ * Default, unprivileged access level.
+ *
+ * <p>Can only access usage for one's own UID.
+ *
+ * <p>Every app will have at least this access level.
+ */
+ int DEFAULT = 0;
+
+ /**
+ * Access level for apps which can access usage for any app running in the same user.
+ *
+ * <p>Granted to:
+ * <ul>
+ * <li>Profile owners.
+ * </ul>
+ */
+ int USER = 1;
+
+ /**
+ * Access level for apps which can access usage summary of device. Device summary includes
+ * usage by apps running in any profiles/users, however this access level does not
+ * allow querying usage of individual apps running in other profiles/users.
+ *
+ * <p>Granted to:
+ * <ul>
+ * <li>Apps with the PACKAGE_USAGE_STATS permission granted. Note that this is an AppOps bit
+ * so it is not necessarily sufficient to declare this in the manifest.
+ * <li>Apps with the (signature/privileged) READ_NETWORK_USAGE_HISTORY permission.
+ * </ul>
+ */
+ int DEVICESUMMARY = 2;
+
+ /**
+ * Access level for apps which can access usage for any app on the device, including apps
+ * running on other users/profiles.
+ *
+ * <p>Granted to:
+ * <ul>
+ * <li>Device owners.
+ * <li>Carrier-privileged applications.
+ * <li>The system UID.
+ * </ul>
+ */
+ int DEVICE = 3;
+ }
+
+ /** Returns the {@link NetworkStatsAccess.Level} for the given caller. */
+ public static @NetworkStatsAccess.Level int checkAccessLevel(
+ Context context, int callingUid, String callingPackage) {
+ final DevicePolicyManagerInternal dpmi = LocalServices.getService(
+ DevicePolicyManagerInternal.class);
+ final TelephonyManager tm = (TelephonyManager)
+ context.getSystemService(Context.TELEPHONY_SERVICE);
+ boolean hasCarrierPrivileges;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ hasCarrierPrivileges = tm != null
+ && tm.checkCarrierPrivilegesForPackageAnyPhone(callingPackage)
+ == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ final boolean isDeviceOwner = dpmi != null && dpmi.isActiveDeviceOwner(callingUid);
+ final int appId = UserHandle.getAppId(callingUid);
+ if (hasCarrierPrivileges || isDeviceOwner
+ || appId == Process.SYSTEM_UID || appId == Process.NETWORK_STACK_UID) {
+ // Carrier-privileged apps and device owners, and the system (including the
+ // network stack) can access data usage for all apps on the device.
+ return NetworkStatsAccess.Level.DEVICE;
+ }
+
+ boolean hasAppOpsPermission = hasAppOpsPermission(context, callingUid, callingPackage);
+ if (hasAppOpsPermission || context.checkCallingOrSelfPermission(
+ READ_NETWORK_USAGE_HISTORY) == PackageManager.PERMISSION_GRANTED) {
+ return NetworkStatsAccess.Level.DEVICESUMMARY;
+ }
+
+ //TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode.
+ boolean isProfileOwner = dpmi != null && (dpmi.isActiveProfileOwner(callingUid)
+ || dpmi.isActiveDeviceOwner(callingUid));
+ if (isProfileOwner) {
+ // Apps with the AppOps permission, profile owners, and apps with the privileged
+ // permission can access data usage for all apps in this user/profile.
+ return NetworkStatsAccess.Level.USER;
+ }
+
+ // Everyone else gets default access (only to their own UID).
+ return NetworkStatsAccess.Level.DEFAULT;
+ }
+
+ /**
+ * Returns whether the given caller should be able to access the given UID when the caller has
+ * the given {@link NetworkStatsAccess.Level}.
+ */
+ public static boolean isAccessibleToUser(int uid, int callerUid,
+ @NetworkStatsAccess.Level int accessLevel) {
+ switch (accessLevel) {
+ case NetworkStatsAccess.Level.DEVICE:
+ // Device-level access - can access usage for any uid.
+ return true;
+ case NetworkStatsAccess.Level.DEVICESUMMARY:
+ // Can access usage for any app running in the same user, along
+ // with some special uids (system, removed, or tethering) and
+ // anonymized uids
+ return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED
+ || uid == UID_TETHERING || uid == UID_ALL
+ || UserHandle.getUserId(uid) == UserHandle.getUserId(callerUid);
+ case NetworkStatsAccess.Level.USER:
+ // User-level access - can access usage for any app running in the same user, along
+ // with some special uids (system, removed, or tethering).
+ return uid == android.os.Process.SYSTEM_UID || uid == UID_REMOVED
+ || uid == UID_TETHERING
+ || UserHandle.getUserId(uid) == UserHandle.getUserId(callerUid);
+ case NetworkStatsAccess.Level.DEFAULT:
+ default:
+ // Default access level - can only access one's own usage.
+ return uid == callerUid;
+ }
+ }
+
+ private static boolean hasAppOpsPermission(
+ Context context, int callingUid, String callingPackage) {
+ if (callingPackage != null) {
+ AppOpsManager appOps = (AppOpsManager) context.getSystemService(
+ Context.APP_OPS_SERVICE);
+
+ final int mode = appOps.noteOp(AppOpsManager.OP_GET_USAGE_STATS,
+ callingUid, callingPackage);
+ if (mode == AppOpsManager.MODE_DEFAULT) {
+ // The default behavior here is to check if PackageManager has given the app
+ // permission.
+ final int permissionCheck = context.checkCallingPermission(
+ Manifest.permission.PACKAGE_USAGE_STATS);
+ return permissionCheck == PackageManager.PERMISSION_GRANTED;
+ }
+ return (mode == AppOpsManager.MODE_ALLOWED);
+ }
+ return false;
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsCollection.java b/service-t/src/com/android/server/net/NetworkStatsCollection.java
new file mode 100644
index 0000000..213ea40
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsCollection.java
@@ -0,0 +1,820 @@
+/*
+ * Copyright (C) 2012 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 static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.ROAMING_YES;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_REMOVED;
+import static android.text.format.DateUtils.WEEK_IN_MILLIS;
+
+import static com.android.internal.net.NetworkUtilsInternal.multiplySafeByRational;
+import static com.android.server.net.NetworkStatsService.TAG;
+
+import android.net.NetworkIdentity;
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TrafficStats;
+import android.os.Binder;
+import android.service.NetworkStatsCollectionKeyProto;
+import android.service.NetworkStatsCollectionProto;
+import android.service.NetworkStatsCollectionStatsProto;
+import android.telephony.SubscriptionPlan;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.IntArray;
+import android.util.MathUtils;
+import android.util.Range;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastDataInput;
+import com.android.internal.util.FastDataOutput;
+import com.android.internal.util.FileRotator;
+import com.android.internal.util.IndentingPrintWriter;
+
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import libcore.io.IoUtils;
+
+import java.io.BufferedInputStream;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.ProtocolException;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Collection of {@link NetworkStatsHistory}, stored based on combined key of
+ * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself.
+ */
+public class NetworkStatsCollection implements FileRotator.Reader, FileRotator.Writer {
+ /** File header magic number: "ANET" */
+ private static final int FILE_MAGIC = 0x414E4554;
+
+ /** Default buffer size from BufferedInputStream */
+ private static final int BUFFER_SIZE = 8192;
+
+ private static final int VERSION_NETWORK_INIT = 1;
+
+ private static final int VERSION_UID_INIT = 1;
+ private static final int VERSION_UID_WITH_IDENT = 2;
+ private static final int VERSION_UID_WITH_TAG = 3;
+ private static final int VERSION_UID_WITH_SET = 4;
+
+ private static final int VERSION_UNIFIED_INIT = 16;
+
+ private ArrayMap<Key, NetworkStatsHistory> mStats = new ArrayMap<>();
+
+ private final long mBucketDuration;
+
+ private long mStartMillis;
+ private long mEndMillis;
+ private long mTotalBytes;
+ private boolean mDirty;
+
+ public NetworkStatsCollection(long bucketDuration) {
+ mBucketDuration = bucketDuration;
+ reset();
+ }
+
+ public void clear() {
+ reset();
+ }
+
+ public void reset() {
+ mStats.clear();
+ mStartMillis = Long.MAX_VALUE;
+ mEndMillis = Long.MIN_VALUE;
+ mTotalBytes = 0;
+ mDirty = false;
+ }
+
+ public long getStartMillis() {
+ return mStartMillis;
+ }
+
+ /**
+ * Return first atomic bucket in this collection, which is more conservative
+ * than {@link #mStartMillis}.
+ */
+ public long getFirstAtomicBucketMillis() {
+ if (mStartMillis == Long.MAX_VALUE) {
+ return Long.MAX_VALUE;
+ } else {
+ return mStartMillis + mBucketDuration;
+ }
+ }
+
+ public long getEndMillis() {
+ return mEndMillis;
+ }
+
+ public long getTotalBytes() {
+ return mTotalBytes;
+ }
+
+ public boolean isDirty() {
+ return mDirty;
+ }
+
+ public void clearDirty() {
+ mDirty = false;
+ }
+
+ public boolean isEmpty() {
+ return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE;
+ }
+
+ @VisibleForTesting
+ public long roundUp(long time) {
+ if (time == Long.MIN_VALUE || time == Long.MAX_VALUE
+ || time == SubscriptionPlan.TIME_UNKNOWN) {
+ return time;
+ } else {
+ final long mod = time % mBucketDuration;
+ if (mod > 0) {
+ time -= mod;
+ time += mBucketDuration;
+ }
+ return time;
+ }
+ }
+
+ @VisibleForTesting
+ public long roundDown(long time) {
+ if (time == Long.MIN_VALUE || time == Long.MAX_VALUE
+ || time == SubscriptionPlan.TIME_UNKNOWN) {
+ return time;
+ } else {
+ final long mod = time % mBucketDuration;
+ if (mod > 0) {
+ time -= mod;
+ }
+ return time;
+ }
+ }
+
+ public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel) {
+ return getRelevantUids(accessLevel, Binder.getCallingUid());
+ }
+
+ public int[] getRelevantUids(@NetworkStatsAccess.Level int accessLevel,
+ final int callerUid) {
+ IntArray uids = new IntArray();
+ for (int i = 0; i < mStats.size(); i++) {
+ final Key key = mStats.keyAt(i);
+ if (NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)) {
+ int j = uids.binarySearch(key.uid);
+
+ if (j < 0) {
+ j = ~j;
+ uids.add(j, key.uid);
+ }
+ }
+ }
+ return uids.toArray();
+ }
+
+ /**
+ * Combine all {@link NetworkStatsHistory} in this collection which match
+ * the requested parameters.
+ */
+ public NetworkStatsHistory getHistory(NetworkTemplate template, SubscriptionPlan augmentPlan,
+ int uid, int set, int tag, int fields, long start, long end,
+ @NetworkStatsAccess.Level int accessLevel, int callerUid) {
+ if (!NetworkStatsAccess.isAccessibleToUser(uid, callerUid, accessLevel)) {
+ throw new SecurityException("Network stats history of uid " + uid
+ + " is forbidden for caller " + callerUid);
+ }
+
+ // 180 days of history should be enough for anyone; if we end up needing
+ // more, we'll dynamically grow the history object.
+ final int bucketEstimate = (int) MathUtils.constrain(((end - start) / mBucketDuration), 0,
+ (180 * DateUtils.DAY_IN_MILLIS) / mBucketDuration);
+ final NetworkStatsHistory combined = new NetworkStatsHistory(
+ mBucketDuration, bucketEstimate, fields);
+
+ // shortcut when we know stats will be empty
+ if (start == end) return combined;
+
+ // Figure out the window of time that we should be augmenting (if any)
+ long augmentStart = SubscriptionPlan.TIME_UNKNOWN;
+ long augmentEnd = (augmentPlan != null) ? augmentPlan.getDataUsageTime()
+ : SubscriptionPlan.TIME_UNKNOWN;
+ // And if augmenting, we might need to collect more data to adjust with
+ long collectStart = start;
+ long collectEnd = end;
+
+ if (augmentEnd != SubscriptionPlan.TIME_UNKNOWN) {
+ final Iterator<Range<ZonedDateTime>> it = augmentPlan.cycleIterator();
+ while (it.hasNext()) {
+ final Range<ZonedDateTime> cycle = it.next();
+ final long cycleStart = cycle.getLower().toInstant().toEpochMilli();
+ final long cycleEnd = cycle.getUpper().toInstant().toEpochMilli();
+ if (cycleStart <= augmentEnd && augmentEnd < cycleEnd) {
+ augmentStart = cycleStart;
+ collectStart = Long.min(collectStart, augmentStart);
+ collectEnd = Long.max(collectEnd, augmentEnd);
+ break;
+ }
+ }
+ }
+
+ if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) {
+ // Shrink augmentation window so we don't risk undercounting.
+ augmentStart = roundUp(augmentStart);
+ augmentEnd = roundDown(augmentEnd);
+ // Grow collection window so we get all the stats needed.
+ collectStart = roundDown(collectStart);
+ collectEnd = roundUp(collectEnd);
+ }
+
+ for (int i = 0; i < mStats.size(); i++) {
+ final Key key = mStats.keyAt(i);
+ if (key.uid == uid && NetworkStats.setMatches(set, key.set) && key.tag == tag
+ && templateMatches(template, key.ident)) {
+ final NetworkStatsHistory value = mStats.valueAt(i);
+ combined.recordHistory(value, collectStart, collectEnd);
+ }
+ }
+
+ if (augmentStart != SubscriptionPlan.TIME_UNKNOWN) {
+ final NetworkStatsHistory.Entry entry = combined.getValues(
+ augmentStart, augmentEnd, null);
+
+ // If we don't have any recorded data for this time period, give
+ // ourselves something to scale with.
+ if (entry.rxBytes == 0 || entry.txBytes == 0) {
+ combined.recordData(augmentStart, augmentEnd,
+ new NetworkStats.Entry(1, 0, 1, 0, 0));
+ combined.getValues(augmentStart, augmentEnd, entry);
+ }
+
+ final long rawBytes = (entry.rxBytes + entry.txBytes) == 0 ? 1 :
+ (entry.rxBytes + entry.txBytes);
+ final long rawRxBytes = entry.rxBytes == 0 ? 1 : entry.rxBytes;
+ final long rawTxBytes = entry.txBytes == 0 ? 1 : entry.txBytes;
+ final long targetBytes = augmentPlan.getDataUsageBytes();
+
+ final long targetRxBytes = multiplySafeByRational(targetBytes, rawRxBytes, rawBytes);
+ final long targetTxBytes = multiplySafeByRational(targetBytes, rawTxBytes, rawBytes);
+
+
+ // Scale all matching buckets to reach anchor target
+ final long beforeTotal = combined.getTotalBytes();
+ for (int i = 0; i < combined.size(); i++) {
+ combined.getValues(i, entry);
+ if (entry.bucketStart >= augmentStart
+ && entry.bucketStart + entry.bucketDuration <= augmentEnd) {
+ entry.rxBytes = multiplySafeByRational(
+ targetRxBytes, entry.rxBytes, rawRxBytes);
+ entry.txBytes = multiplySafeByRational(
+ targetTxBytes, entry.txBytes, rawTxBytes);
+ // We purposefully clear out packet counters to indicate
+ // that this data has been augmented.
+ entry.rxPackets = 0;
+ entry.txPackets = 0;
+ combined.setValues(i, entry);
+ }
+ }
+
+ final long deltaTotal = combined.getTotalBytes() - beforeTotal;
+ if (deltaTotal != 0) {
+ Slog.d(TAG, "Augmented network usage by " + deltaTotal + " bytes");
+ }
+
+ // Finally we can slice data as originally requested
+ final NetworkStatsHistory sliced = new NetworkStatsHistory(
+ mBucketDuration, bucketEstimate, fields);
+ sliced.recordHistory(combined, start, end);
+ return sliced;
+ } else {
+ return combined;
+ }
+ }
+
+ /**
+ * Summarize all {@link NetworkStatsHistory} in this collection which match
+ * the requested parameters across the requested range.
+ *
+ * @param template - a predicate for filtering netstats.
+ * @param start - start of the range, timestamp in milliseconds since the epoch.
+ * @param end - end of the range, timestamp in milliseconds since the epoch.
+ * @param accessLevel - caller access level.
+ * @param callerUid - caller UID.
+ */
+ public NetworkStats getSummary(NetworkTemplate template, long start, long end,
+ @NetworkStatsAccess.Level int accessLevel, int callerUid) {
+ final long now = System.currentTimeMillis();
+
+ final NetworkStats stats = new NetworkStats(end - start, 24);
+
+ // shortcut when we know stats will be empty
+ if (start == end) return stats;
+
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+ NetworkStatsHistory.Entry historyEntry = null;
+
+ for (int i = 0; i < mStats.size(); i++) {
+ final Key key = mStats.keyAt(i);
+ if (templateMatches(template, key.ident)
+ && NetworkStatsAccess.isAccessibleToUser(key.uid, callerUid, accessLevel)
+ && key.set < NetworkStats.SET_DEBUG_START) {
+ final NetworkStatsHistory value = mStats.valueAt(i);
+ historyEntry = value.getValues(start, end, now, historyEntry);
+
+ entry.iface = IFACE_ALL;
+ entry.uid = key.uid;
+ entry.set = key.set;
+ entry.tag = key.tag;
+ entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork() ?
+ DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO;
+ entry.metered = key.ident.isAnyMemberMetered() ? METERED_YES : METERED_NO;
+ entry.roaming = key.ident.isAnyMemberRoaming() ? ROAMING_YES : ROAMING_NO;
+ entry.rxBytes = historyEntry.rxBytes;
+ entry.rxPackets = historyEntry.rxPackets;
+ entry.txBytes = historyEntry.txBytes;
+ entry.txPackets = historyEntry.txPackets;
+ entry.operations = historyEntry.operations;
+
+ if (!entry.isEmpty()) {
+ stats.combineValues(entry);
+ }
+ }
+ }
+
+ return stats;
+ }
+
+ /**
+ * Record given {@link android.net.NetworkStats.Entry} into this collection.
+ */
+ public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start,
+ long end, NetworkStats.Entry entry) {
+ final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag);
+ history.recordData(start, end, entry);
+ noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes);
+ }
+
+ /**
+ * Record given {@link NetworkStatsHistory} into this collection.
+ */
+ private void recordHistory(Key key, NetworkStatsHistory history) {
+ if (history.size() == 0) return;
+ noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes());
+
+ NetworkStatsHistory target = mStats.get(key);
+ if (target == null) {
+ target = new NetworkStatsHistory(history.getBucketDuration());
+ mStats.put(key, target);
+ }
+ target.recordEntireHistory(history);
+ }
+
+ /**
+ * Record all {@link NetworkStatsHistory} contained in the given collection
+ * into this collection.
+ */
+ public void recordCollection(NetworkStatsCollection another) {
+ for (int i = 0; i < another.mStats.size(); i++) {
+ final Key key = another.mStats.keyAt(i);
+ final NetworkStatsHistory value = another.mStats.valueAt(i);
+ recordHistory(key, value);
+ }
+ }
+
+ private NetworkStatsHistory findOrCreateHistory(
+ NetworkIdentitySet ident, int uid, int set, int tag) {
+ final Key key = new Key(ident, uid, set, tag);
+ final NetworkStatsHistory existing = mStats.get(key);
+
+ // update when no existing, or when bucket duration changed
+ NetworkStatsHistory updated = null;
+ if (existing == null) {
+ updated = new NetworkStatsHistory(mBucketDuration, 10);
+ } else if (existing.getBucketDuration() != mBucketDuration) {
+ updated = new NetworkStatsHistory(existing, mBucketDuration);
+ }
+
+ if (updated != null) {
+ mStats.put(key, updated);
+ return updated;
+ } else {
+ return existing;
+ }
+ }
+
+ @Override
+ public void read(InputStream in) throws IOException {
+ final FastDataInput dataIn = new FastDataInput(in, BUFFER_SIZE);
+ read(dataIn);
+ }
+
+ private void read(DataInput in) throws IOException {
+ // verify file magic header intact
+ final int magic = in.readInt();
+ if (magic != FILE_MAGIC) {
+ throw new ProtocolException("unexpected magic: " + magic);
+ }
+
+ final int version = in.readInt();
+ switch (version) {
+ case VERSION_UNIFIED_INIT: {
+ // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
+ final int identSize = in.readInt();
+ for (int i = 0; i < identSize; i++) {
+ final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+
+ final int size = in.readInt();
+ for (int j = 0; j < size; j++) {
+ final int uid = in.readInt();
+ final int set = in.readInt();
+ final int tag = in.readInt();
+
+ final Key key = new Key(ident, uid, set, tag);
+ final NetworkStatsHistory history = new NetworkStatsHistory(in);
+ recordHistory(key, history);
+ }
+ }
+ break;
+ }
+ default: {
+ throw new ProtocolException("unexpected version: " + version);
+ }
+ }
+ }
+
+ @Override
+ public void write(OutputStream out) throws IOException {
+ final FastDataOutput dataOut = new FastDataOutput(out, BUFFER_SIZE);
+ write(dataOut);
+ dataOut.flush();
+ }
+
+ private void write(DataOutput out) throws IOException {
+ // cluster key lists grouped by ident
+ final HashMap<NetworkIdentitySet, ArrayList<Key>> keysByIdent = Maps.newHashMap();
+ for (Key key : mStats.keySet()) {
+ ArrayList<Key> keys = keysByIdent.get(key.ident);
+ if (keys == null) {
+ keys = Lists.newArrayList();
+ keysByIdent.put(key.ident, keys);
+ }
+ keys.add(key);
+ }
+
+ out.writeInt(FILE_MAGIC);
+ out.writeInt(VERSION_UNIFIED_INIT);
+
+ out.writeInt(keysByIdent.size());
+ for (NetworkIdentitySet ident : keysByIdent.keySet()) {
+ final ArrayList<Key> keys = keysByIdent.get(ident);
+ ident.writeToStream(out);
+
+ out.writeInt(keys.size());
+ for (Key key : keys) {
+ final NetworkStatsHistory history = mStats.get(key);
+ out.writeInt(key.uid);
+ out.writeInt(key.set);
+ out.writeInt(key.tag);
+ history.writeToStream(out);
+ }
+ }
+ }
+
+ @Deprecated
+ public void readLegacyNetwork(File file) throws IOException {
+ final AtomicFile inputFile = new AtomicFile(file);
+
+ DataInputStream in = null;
+ try {
+ in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
+
+ // verify file magic header intact
+ final int magic = in.readInt();
+ if (magic != FILE_MAGIC) {
+ throw new ProtocolException("unexpected magic: " + magic);
+ }
+
+ final int version = in.readInt();
+ switch (version) {
+ case VERSION_NETWORK_INIT: {
+ // network := size *(NetworkIdentitySet NetworkStatsHistory)
+ final int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+ final NetworkStatsHistory history = new NetworkStatsHistory(in);
+
+ final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE);
+ recordHistory(key, history);
+ }
+ break;
+ }
+ default: {
+ throw new ProtocolException("unexpected version: " + version);
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // missing stats is okay, probably first boot
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ }
+
+ @Deprecated
+ public void readLegacyUid(File file, boolean onlyTags) throws IOException {
+ final AtomicFile inputFile = new AtomicFile(file);
+
+ DataInputStream in = null;
+ try {
+ in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
+
+ // verify file magic header intact
+ final int magic = in.readInt();
+ if (magic != FILE_MAGIC) {
+ throw new ProtocolException("unexpected magic: " + magic);
+ }
+
+ final int version = in.readInt();
+ switch (version) {
+ case VERSION_UID_INIT: {
+ // uid := size *(UID NetworkStatsHistory)
+
+ // drop this data version, since we don't have a good
+ // mapping into NetworkIdentitySet.
+ break;
+ }
+ case VERSION_UID_WITH_IDENT: {
+ // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory))
+
+ // drop this data version, since this version only existed
+ // for a short time.
+ break;
+ }
+ case VERSION_UID_WITH_TAG:
+ case VERSION_UID_WITH_SET: {
+ // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
+ final int identSize = in.readInt();
+ for (int i = 0; i < identSize; i++) {
+ final NetworkIdentitySet ident = new NetworkIdentitySet(in);
+
+ final int size = in.readInt();
+ for (int j = 0; j < size; j++) {
+ final int uid = in.readInt();
+ final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt()
+ : SET_DEFAULT;
+ final int tag = in.readInt();
+
+ final Key key = new Key(ident, uid, set, tag);
+ final NetworkStatsHistory history = new NetworkStatsHistory(in);
+
+ if ((tag == TAG_NONE) != onlyTags) {
+ recordHistory(key, history);
+ }
+ }
+ }
+ break;
+ }
+ default: {
+ throw new ProtocolException("unexpected version: " + version);
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // missing stats is okay, probably first boot
+ } finally {
+ IoUtils.closeQuietly(in);
+ }
+ }
+
+ /**
+ * Remove any {@link NetworkStatsHistory} attributed to the requested UID,
+ * moving any {@link NetworkStats#TAG_NONE} series to
+ * {@link TrafficStats#UID_REMOVED}.
+ */
+ public void removeUids(int[] uids) {
+ final ArrayList<Key> knownKeys = Lists.newArrayList();
+ knownKeys.addAll(mStats.keySet());
+
+ // migrate all UID stats into special "removed" bucket
+ for (Key key : knownKeys) {
+ if (ArrayUtils.contains(uids, key.uid)) {
+ // only migrate combined TAG_NONE history
+ if (key.tag == TAG_NONE) {
+ final NetworkStatsHistory uidHistory = mStats.get(key);
+ final NetworkStatsHistory removedHistory = findOrCreateHistory(
+ key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE);
+ removedHistory.recordEntireHistory(uidHistory);
+ }
+ mStats.remove(key);
+ mDirty = true;
+ }
+ }
+ }
+
+ private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
+ if (startMillis < mStartMillis) mStartMillis = startMillis;
+ if (endMillis > mEndMillis) mEndMillis = endMillis;
+ mTotalBytes += totalBytes;
+ mDirty = true;
+ }
+
+ private int estimateBuckets() {
+ return (int) (Math.min(mEndMillis - mStartMillis, WEEK_IN_MILLIS * 5)
+ / mBucketDuration);
+ }
+
+ private ArrayList<Key> getSortedKeys() {
+ final ArrayList<Key> keys = Lists.newArrayList();
+ keys.addAll(mStats.keySet());
+ Collections.sort(keys);
+ return keys;
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ for (Key key : getSortedKeys()) {
+ pw.print("ident="); pw.print(key.ident.toString());
+ pw.print(" uid="); pw.print(key.uid);
+ pw.print(" set="); pw.print(NetworkStats.setToString(key.set));
+ pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag));
+
+ final NetworkStatsHistory history = mStats.get(key);
+ pw.increaseIndent();
+ history.dump(pw, true);
+ pw.decreaseIndent();
+ }
+ }
+
+ public void dumpDebug(ProtoOutputStream proto, long tag) {
+ final long start = proto.start(tag);
+
+ for (Key key : getSortedKeys()) {
+ final long startStats = proto.start(NetworkStatsCollectionProto.STATS);
+
+ // Key
+ final long startKey = proto.start(NetworkStatsCollectionStatsProto.KEY);
+ key.ident.dumpDebug(proto, NetworkStatsCollectionKeyProto.IDENTITY);
+ proto.write(NetworkStatsCollectionKeyProto.UID, key.uid);
+ proto.write(NetworkStatsCollectionKeyProto.SET, key.set);
+ proto.write(NetworkStatsCollectionKeyProto.TAG, key.tag);
+ proto.end(startKey);
+
+ // Value
+ final NetworkStatsHistory history = mStats.get(key);
+ history.dumpDebug(proto, NetworkStatsCollectionStatsProto.HISTORY);
+ proto.end(startStats);
+ }
+
+ proto.end(start);
+ }
+
+ public void dumpCheckin(PrintWriter pw, long start, long end) {
+ dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateMobileWildcard(), "cell");
+ dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateWifiWildcard(), "wifi");
+ dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateEthernet(), "eth");
+ dumpCheckin(pw, start, end, NetworkTemplate.buildTemplateBluetooth(), "bt");
+ }
+
+ /**
+ * Dump all contained stats that match requested parameters, but group
+ * together all matching {@link NetworkTemplate} under a single prefix.
+ */
+ private void dumpCheckin(PrintWriter pw, long start, long end, NetworkTemplate groupTemplate,
+ String groupPrefix) {
+ final ArrayMap<Key, NetworkStatsHistory> grouped = new ArrayMap<>();
+
+ // Walk through all history, grouping by matching network templates
+ for (int i = 0; i < mStats.size(); i++) {
+ final Key key = mStats.keyAt(i);
+ final NetworkStatsHistory value = mStats.valueAt(i);
+
+ if (!templateMatches(groupTemplate, key.ident)) continue;
+ if (key.set >= NetworkStats.SET_DEBUG_START) continue;
+
+ final Key groupKey = new Key(null, key.uid, key.set, key.tag);
+ NetworkStatsHistory groupHistory = grouped.get(groupKey);
+ if (groupHistory == null) {
+ groupHistory = new NetworkStatsHistory(value.getBucketDuration());
+ grouped.put(groupKey, groupHistory);
+ }
+ groupHistory.recordHistory(value, start, end);
+ }
+
+ for (int i = 0; i < grouped.size(); i++) {
+ final Key key = grouped.keyAt(i);
+ final NetworkStatsHistory value = grouped.valueAt(i);
+
+ if (value.size() == 0) continue;
+
+ pw.print("c,");
+ pw.print(groupPrefix); pw.print(',');
+ pw.print(key.uid); pw.print(',');
+ pw.print(NetworkStats.setToCheckinString(key.set)); pw.print(',');
+ pw.print(key.tag);
+ pw.println();
+
+ value.dumpCheckin(pw);
+ }
+ }
+
+ /**
+ * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
+ * in the given {@link NetworkIdentitySet}.
+ */
+ private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) {
+ for (NetworkIdentity ident : identSet) {
+ if (template.matches(ident)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static class Key implements Comparable<Key> {
+ public final NetworkIdentitySet ident;
+ public final int uid;
+ public final int set;
+ public final int tag;
+
+ private final int hashCode;
+
+ public Key(NetworkIdentitySet ident, int uid, int set, int tag) {
+ this.ident = ident;
+ this.uid = uid;
+ this.set = set;
+ this.tag = tag;
+ hashCode = Objects.hash(ident, uid, set, tag);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Key) {
+ final Key key = (Key) obj;
+ return uid == key.uid && set == key.set && tag == key.tag
+ && Objects.equals(ident, key.ident);
+ }
+ return false;
+ }
+
+ @Override
+ public int compareTo(Key another) {
+ int res = 0;
+ if (ident != null && another.ident != null) {
+ res = ident.compareTo(another.ident);
+ }
+ if (res == 0) {
+ res = Integer.compare(uid, another.uid);
+ }
+ if (res == 0) {
+ res = Integer.compare(set, another.set);
+ }
+ if (res == 0) {
+ res = Integer.compare(tag, another.tag);
+ }
+ return res;
+ }
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsFactory.java b/service-t/src/com/android/server/net/NetworkStatsFactory.java
new file mode 100644
index 0000000..e6433db
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsFactory.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2011 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 static android.net.NetworkStats.INTERFACES_ALL;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.TAG_ALL;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+
+import static com.android.server.NetworkManagementSocketTagger.kernelToTag;
+
+import android.annotation.Nullable;
+import android.net.INetd;
+import android.net.NetworkStats;
+import android.net.UnderlyingNetworkInfo;
+import android.net.util.NetdService;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.ProcFileReader;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Creates {@link NetworkStats} instances by parsing various {@code /proc/}
+ * files as needed.
+ *
+ * @hide
+ */
+public class NetworkStatsFactory {
+ private static final String TAG = "NetworkStatsFactory";
+
+ private static final boolean USE_NATIVE_PARSING = true;
+ private static final boolean VALIDATE_NATIVE_STATS = false;
+
+ /** Path to {@code /proc/net/xt_qtaguid/iface_stat_all}. */
+ private final File mStatsXtIfaceAll;
+ /** Path to {@code /proc/net/xt_qtaguid/iface_stat_fmt}. */
+ private final File mStatsXtIfaceFmt;
+ /** Path to {@code /proc/net/xt_qtaguid/stats}. */
+ private final File mStatsXtUid;
+
+ private final boolean mUseBpfStats;
+
+ private INetd mNetdService;
+
+ /**
+ * Guards persistent data access in this class
+ *
+ * <p>In order to prevent deadlocks, critical sections protected by this lock SHALL NOT call out
+ * to other code that will acquire other locks within the system server. See b/134244752.
+ */
+ private final Object mPersistentDataLock = new Object();
+
+ /** Set containing info about active VPNs and their underlying networks. */
+ private volatile UnderlyingNetworkInfo[] mUnderlyingNetworkInfos = new UnderlyingNetworkInfo[0];
+
+ // A persistent snapshot of cumulative stats since device start
+ @GuardedBy("mPersistentDataLock")
+ private NetworkStats mPersistSnapshot;
+
+ // The persistent snapshot of tun and 464xlat adjusted stats since device start
+ @GuardedBy("mPersistentDataLock")
+ private NetworkStats mTunAnd464xlatAdjustedStats;
+
+ /**
+ * (Stacked interface) -> (base interface) association for all connected ifaces since boot.
+ *
+ * Because counters must never roll backwards, once a given interface is stacked on top of an
+ * underlying interface, the stacked interface can never be stacked on top of
+ * another interface. */
+ private final ConcurrentHashMap<String, String> mStackedIfaces
+ = new ConcurrentHashMap<>();
+
+ /** Informs the factory of a new stacked interface. */
+ public void noteStackedIface(String stackedIface, String baseIface) {
+ if (stackedIface != null && baseIface != null) {
+ mStackedIfaces.put(stackedIface, baseIface);
+ }
+ }
+
+ /**
+ * Set active VPN information for data usage migration purposes
+ *
+ * <p>Traffic on TUN-based VPNs inherently all appear to be originated from the VPN providing
+ * app's UID. This method is used to support migration of VPN data usage, ensuring data is
+ * accurately billed to the real owner of the traffic.
+ *
+ * @param vpnArray The snapshot of the currently-running VPNs.
+ */
+ public void updateUnderlyingNetworkInfos(UnderlyingNetworkInfo[] vpnArray) {
+ mUnderlyingNetworkInfos = vpnArray.clone();
+ }
+
+ /**
+ * Get a set of interfaces containing specified ifaces and stacked interfaces.
+ *
+ * <p>The added stacked interfaces are ifaces stacked on top of the specified ones, or ifaces
+ * on which the specified ones are stacked. Stacked interfaces are those noted with
+ * {@link #noteStackedIface(String, String)}, but only interfaces noted before this method
+ * is called are guaranteed to be included.
+ */
+ public String[] augmentWithStackedInterfaces(@Nullable String[] requiredIfaces) {
+ if (requiredIfaces == NetworkStats.INTERFACES_ALL) {
+ return null;
+ }
+
+ HashSet<String> relatedIfaces = new HashSet<>(Arrays.asList(requiredIfaces));
+ // ConcurrentHashMap's EntrySet iterators are "guaranteed to traverse
+ // elements as they existed upon construction exactly once, and may
+ // (but are not guaranteed to) reflect any modifications subsequent to construction".
+ // This is enough here.
+ for (Map.Entry<String, String> entry : mStackedIfaces.entrySet()) {
+ if (relatedIfaces.contains(entry.getKey())) {
+ relatedIfaces.add(entry.getValue());
+ } else if (relatedIfaces.contains(entry.getValue())) {
+ relatedIfaces.add(entry.getKey());
+ }
+ }
+
+ String[] outArray = new String[relatedIfaces.size()];
+ return relatedIfaces.toArray(outArray);
+ }
+
+ /**
+ * Applies 464xlat adjustments with ifaces noted with {@link #noteStackedIface(String, String)}.
+ * @see NetworkStats#apply464xlatAdjustments(NetworkStats, NetworkStats, Map)
+ */
+ public void apply464xlatAdjustments(NetworkStats baseTraffic, NetworkStats stackedTraffic) {
+ NetworkStats.apply464xlatAdjustments(baseTraffic, stackedTraffic, mStackedIfaces);
+ }
+
+ public NetworkStatsFactory() {
+ this(new File("/proc/"), true);
+ }
+
+ @VisibleForTesting
+ public NetworkStatsFactory(File procRoot, boolean useBpfStats) {
+ mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
+ mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
+ mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
+ mUseBpfStats = useBpfStats;
+ synchronized (mPersistentDataLock) {
+ mPersistSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), -1);
+ mTunAnd464xlatAdjustedStats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
+ }
+ }
+
+ public NetworkStats readBpfNetworkStatsDev() throws IOException {
+ final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+ if (nativeReadNetworkStatsDev(stats) != 0) {
+ throw new IOException("Failed to parse bpf iface stats");
+ }
+ return stats;
+ }
+
+ /**
+ * Parse and return interface-level summary {@link NetworkStats} measured
+ * using {@code /proc/net/dev} style hooks, which may include non IP layer
+ * traffic. Values monotonically increase since device boot, and may include
+ * details about inactive interfaces.
+ *
+ * @throws IllegalStateException when problem parsing stats.
+ */
+ public NetworkStats readNetworkStatsSummaryDev() throws IOException {
+
+ // Return xt_bpf stats if switched to bpf module.
+ if (mUseBpfStats)
+ return readBpfNetworkStatsDev();
+
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+ final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+ ProcFileReader reader = null;
+ try {
+ reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceAll));
+
+ while (reader.hasMoreData()) {
+ entry.iface = reader.nextString();
+ entry.uid = UID_ALL;
+ entry.set = SET_ALL;
+ entry.tag = TAG_NONE;
+
+ final boolean active = reader.nextInt() != 0;
+
+ // always include snapshot values
+ entry.rxBytes = reader.nextLong();
+ entry.rxPackets = reader.nextLong();
+ entry.txBytes = reader.nextLong();
+ entry.txPackets = reader.nextLong();
+
+ // fold in active numbers, but only when active
+ if (active) {
+ entry.rxBytes += reader.nextLong();
+ entry.rxPackets += reader.nextLong();
+ entry.txBytes += reader.nextLong();
+ entry.txPackets += reader.nextLong();
+ }
+
+ stats.insertEntry(entry);
+ reader.finishLine();
+ }
+ } catch (NullPointerException|NumberFormatException e) {
+ throw protocolExceptionWithCause("problem parsing stats", e);
+ } finally {
+ IoUtils.closeQuietly(reader);
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ return stats;
+ }
+
+ /**
+ * Parse and return interface-level summary {@link NetworkStats}. Designed
+ * to return only IP layer traffic. Values monotonically increase since
+ * device boot, and may include details about inactive interfaces.
+ *
+ * @throws IllegalStateException when problem parsing stats.
+ */
+ public NetworkStats readNetworkStatsSummaryXt() throws IOException {
+
+ // Return xt_bpf stats if qtaguid module is replaced.
+ if (mUseBpfStats)
+ return readBpfNetworkStatsDev();
+
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+ // return null when kernel doesn't support
+ if (!mStatsXtIfaceFmt.exists()) return null;
+
+ final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+ ProcFileReader reader = null;
+ try {
+ // open and consume header line
+ reader = new ProcFileReader(new FileInputStream(mStatsXtIfaceFmt));
+ reader.finishLine();
+
+ while (reader.hasMoreData()) {
+ entry.iface = reader.nextString();
+ entry.uid = UID_ALL;
+ entry.set = SET_ALL;
+ entry.tag = TAG_NONE;
+
+ entry.rxBytes = reader.nextLong();
+ entry.rxPackets = reader.nextLong();
+ entry.txBytes = reader.nextLong();
+ entry.txPackets = reader.nextLong();
+
+ stats.insertEntry(entry);
+ reader.finishLine();
+ }
+ } catch (NullPointerException|NumberFormatException e) {
+ throw protocolExceptionWithCause("problem parsing stats", e);
+ } finally {
+ IoUtils.closeQuietly(reader);
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ return stats;
+ }
+
+ public NetworkStats readNetworkStatsDetail() throws IOException {
+ return readNetworkStatsDetail(UID_ALL, INTERFACES_ALL, TAG_ALL);
+ }
+
+ @GuardedBy("mPersistentDataLock")
+ private void requestSwapActiveStatsMapLocked() throws RemoteException {
+ // Ask netd to do a active map stats swap. When the binder call successfully returns,
+ // the system server should be able to safely read and clean the inactive map
+ // without race problem.
+ if (mNetdService == null) {
+ mNetdService = NetdService.getInstance();
+ }
+ mNetdService.trafficSwapActiveStatsMap();
+ }
+
+ /**
+ * Reads the detailed UID stats based on the provided parameters
+ *
+ * @param limitUid the UID to limit this query to
+ * @param limitIfaces the interfaces to limit this query to. Use {@link
+ * NetworkStats.INTERFACES_ALL} to select all interfaces
+ * @param limitTag the tags to limit this query to
+ * @return the NetworkStats instance containing network statistics at the present time.
+ */
+ public NetworkStats readNetworkStatsDetail(
+ int limitUid, String[] limitIfaces, int limitTag) throws IOException {
+ // In order to prevent deadlocks, anything protected by this lock MUST NOT call out to other
+ // code that will acquire other locks within the system server. See b/134244752.
+ synchronized (mPersistentDataLock) {
+ // Take a reference. If this gets swapped out, we still have the old reference.
+ final UnderlyingNetworkInfo[] vpnArray = mUnderlyingNetworkInfos;
+ // Take a defensive copy. mPersistSnapshot is mutated in some cases below
+ final NetworkStats prev = mPersistSnapshot.clone();
+
+ if (USE_NATIVE_PARSING) {
+ final NetworkStats stats =
+ new NetworkStats(SystemClock.elapsedRealtime(), 0 /* initialSize */);
+ if (mUseBpfStats) {
+ try {
+ requestSwapActiveStatsMapLocked();
+ } catch (RemoteException e) {
+ throw new IOException(e);
+ }
+ // Stats are always read from the inactive map, so they must be read after the
+ // swap
+ if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
+ INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
+ throw new IOException("Failed to parse network stats");
+ }
+
+ // BPF stats are incremental; fold into mPersistSnapshot.
+ mPersistSnapshot.setElapsedRealtime(stats.getElapsedRealtime());
+ mPersistSnapshot.combineAllValues(stats);
+ } else {
+ if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), UID_ALL,
+ INTERFACES_ALL, TAG_ALL, mUseBpfStats) != 0) {
+ throw new IOException("Failed to parse network stats");
+ }
+ if (VALIDATE_NATIVE_STATS) {
+ final NetworkStats javaStats = javaReadNetworkStatsDetail(mStatsXtUid,
+ UID_ALL, INTERFACES_ALL, TAG_ALL);
+ assertEquals(javaStats, stats);
+ }
+
+ mPersistSnapshot = stats;
+ }
+ } else {
+ mPersistSnapshot = javaReadNetworkStatsDetail(mStatsXtUid, UID_ALL, INTERFACES_ALL,
+ TAG_ALL);
+ }
+
+ NetworkStats adjustedStats = adjustForTunAnd464Xlat(mPersistSnapshot, prev, vpnArray);
+
+ // Filter return values
+ adjustedStats.filter(limitUid, limitIfaces, limitTag);
+ return adjustedStats;
+ }
+ }
+
+ @GuardedBy("mPersistentDataLock")
+ private NetworkStats adjustForTunAnd464Xlat(NetworkStats uidDetailStats,
+ NetworkStats previousStats, UnderlyingNetworkInfo[] vpnArray) {
+ // Calculate delta from last snapshot
+ final NetworkStats delta = uidDetailStats.subtract(previousStats);
+
+ // Apply 464xlat adjustments before VPN adjustments. If VPNs are using v4 on a v6 only
+ // network, the overhead is their fault.
+ // No locking here: apply464xlatAdjustments behaves fine with an add-only
+ // ConcurrentHashMap.
+ delta.apply464xlatAdjustments(mStackedIfaces);
+
+ // Migrate data usage over a VPN to the TUN network.
+ for (UnderlyingNetworkInfo info : vpnArray) {
+ delta.migrateTun(info.getOwnerUid(), info.getInterface(),
+ info.getUnderlyingInterfaces());
+ // Filter out debug entries as that may lead to over counting.
+ delta.filterDebugEntries();
+ }
+
+ // Update mTunAnd464xlatAdjustedStats with migrated delta.
+ mTunAnd464xlatAdjustedStats.combineAllValues(delta);
+ mTunAnd464xlatAdjustedStats.setElapsedRealtime(uidDetailStats.getElapsedRealtime());
+
+ return mTunAnd464xlatAdjustedStats.clone();
+ }
+
+ /**
+ * Parse and return {@link NetworkStats} with UID-level details. Values are
+ * expected to monotonically increase since device boot.
+ */
+ @VisibleForTesting
+ public static NetworkStats javaReadNetworkStatsDetail(File detailPath, int limitUid,
+ String[] limitIfaces, int limitTag)
+ throws IOException {
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+ final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 24);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+ int idx = 1;
+ int lastIdx = 1;
+
+ ProcFileReader reader = null;
+ try {
+ // open and consume header line
+ reader = new ProcFileReader(new FileInputStream(detailPath));
+ reader.finishLine();
+
+ while (reader.hasMoreData()) {
+ idx = reader.nextInt();
+ if (idx != lastIdx + 1) {
+ throw new ProtocolException(
+ "inconsistent idx=" + idx + " after lastIdx=" + lastIdx);
+ }
+ lastIdx = idx;
+
+ entry.iface = reader.nextString();
+ entry.tag = kernelToTag(reader.nextString());
+ entry.uid = reader.nextInt();
+ entry.set = reader.nextInt();
+ entry.rxBytes = reader.nextLong();
+ entry.rxPackets = reader.nextLong();
+ entry.txBytes = reader.nextLong();
+ entry.txPackets = reader.nextLong();
+
+ if ((limitIfaces == null || ArrayUtils.contains(limitIfaces, entry.iface))
+ && (limitUid == UID_ALL || limitUid == entry.uid)
+ && (limitTag == TAG_ALL || limitTag == entry.tag)) {
+ stats.insertEntry(entry);
+ }
+
+ reader.finishLine();
+ }
+ } catch (NullPointerException|NumberFormatException e) {
+ throw protocolExceptionWithCause("problem parsing idx " + idx, e);
+ } finally {
+ IoUtils.closeQuietly(reader);
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+
+ return stats;
+ }
+
+ public void assertEquals(NetworkStats expected, NetworkStats actual) {
+ if (expected.size() != actual.size()) {
+ throw new AssertionError(
+ "Expected size " + expected.size() + ", actual size " + actual.size());
+ }
+
+ NetworkStats.Entry expectedRow = null;
+ NetworkStats.Entry actualRow = null;
+ for (int i = 0; i < expected.size(); i++) {
+ expectedRow = expected.getValues(i, expectedRow);
+ actualRow = actual.getValues(i, actualRow);
+ if (!expectedRow.equals(actualRow)) {
+ throw new AssertionError(
+ "Expected row " + i + ": " + expectedRow + ", actual row " + actualRow);
+ }
+ }
+ }
+
+ /**
+ * Parse statistics from file into given {@link NetworkStats} object. Values
+ * are expected to monotonically increase since device boot.
+ */
+ @VisibleForTesting
+ public static native int nativeReadNetworkStatsDetail(NetworkStats stats, String path,
+ int limitUid, String[] limitIfaces, int limitTag, boolean useBpfStats);
+
+ @VisibleForTesting
+ public static native int nativeReadNetworkStatsDev(NetworkStats stats);
+
+ private static ProtocolException protocolExceptionWithCause(String message, Throwable cause) {
+ ProtocolException pe = new ProtocolException(message);
+ pe.initCause(cause);
+ return pe;
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
new file mode 100644
index 0000000..2564dae
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2016 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 static android.app.usage.NetworkStatsManager.MIN_THRESHOLD_BYTES;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.app.usage.NetworkStatsManager;
+import android.net.DataUsageRequest;
+import android.net.NetworkStats;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Manages observers of {@link NetworkStats}. Allows observers to be notified when
+ * data usage has been reported in {@link NetworkStatsService}. An observer can set
+ * a threshold of how much data it cares about to be notified.
+ */
+class NetworkStatsObservers {
+ private static final String TAG = "NetworkStatsObservers";
+ private static final boolean LOGV = false;
+
+ private static final int MSG_REGISTER = 1;
+ private static final int MSG_UNREGISTER = 2;
+ private static final int MSG_UPDATE_STATS = 3;
+
+ // All access to this map must be done from the handler thread.
+ // indexed by DataUsageRequest#requestId
+ private final SparseArray<RequestInfo> mDataUsageRequests = new SparseArray<>();
+
+ // Sequence number of DataUsageRequests
+ private final AtomicInteger mNextDataUsageRequestId = new AtomicInteger();
+
+ // Lazily instantiated when an observer is registered.
+ private volatile Handler mHandler;
+
+ /**
+ * Creates a wrapper that contains the caller context and a normalized request.
+ * The request should be returned to the caller app, and the wrapper should be sent to this
+ * object through #addObserver by the service handler.
+ *
+ * <p>It will register the observer asynchronously, so it is safe to call from any thread.
+ *
+ * @return the normalized request wrapped within {@link RequestInfo}.
+ */
+ public DataUsageRequest register(DataUsageRequest inputRequest, Messenger messenger,
+ IBinder binder, int callingUid, @NetworkStatsAccess.Level int accessLevel) {
+ DataUsageRequest request = buildRequest(inputRequest);
+ RequestInfo requestInfo = buildRequestInfo(request, messenger, binder, callingUid,
+ accessLevel);
+
+ if (LOGV) Slog.v(TAG, "Registering observer for " + request);
+ getHandler().sendMessage(mHandler.obtainMessage(MSG_REGISTER, requestInfo));
+ return request;
+ }
+
+ /**
+ * Unregister a data usage observer.
+ *
+ * <p>It will unregister the observer asynchronously, so it is safe to call from any thread.
+ */
+ public void unregister(DataUsageRequest request, int callingUid) {
+ getHandler().sendMessage(mHandler.obtainMessage(MSG_UNREGISTER, callingUid, 0 /* ignore */,
+ request));
+ }
+
+ /**
+ * Updates data usage statistics of registered observers and notifies if limits are reached.
+ *
+ * <p>It will update stats asynchronously, so it is safe to call from any thread.
+ */
+ public void updateStats(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
+ ArrayMap<String, NetworkIdentitySet> activeIfaces,
+ ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
+ long currentTime) {
+ StatsContext statsContext = new StatsContext(xtSnapshot, uidSnapshot, activeIfaces,
+ activeUidIfaces, currentTime);
+ getHandler().sendMessage(mHandler.obtainMessage(MSG_UPDATE_STATS, statsContext));
+ }
+
+ private Handler getHandler() {
+ if (mHandler == null) {
+ synchronized (this) {
+ if (mHandler == null) {
+ if (LOGV) Slog.v(TAG, "Creating handler");
+ mHandler = new Handler(getHandlerLooperLocked(), mHandlerCallback);
+ }
+ }
+ }
+ return mHandler;
+ }
+
+ @VisibleForTesting
+ protected Looper getHandlerLooperLocked() {
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ return handlerThread.getLooper();
+ }
+
+ private Handler.Callback mHandlerCallback = new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_REGISTER: {
+ handleRegister((RequestInfo) msg.obj);
+ return true;
+ }
+ case MSG_UNREGISTER: {
+ handleUnregister((DataUsageRequest) msg.obj, msg.arg1 /* callingUid */);
+ return true;
+ }
+ case MSG_UPDATE_STATS: {
+ handleUpdateStats((StatsContext) msg.obj);
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+ };
+
+ /**
+ * Adds a {@link RequestInfo} as an observer.
+ * Should only be called from the handler thread otherwise there will be a race condition
+ * on mDataUsageRequests.
+ */
+ private void handleRegister(RequestInfo requestInfo) {
+ mDataUsageRequests.put(requestInfo.mRequest.requestId, requestInfo);
+ }
+
+ /**
+ * Removes a {@link DataUsageRequest} if the calling uid is authorized.
+ * Should only be called from the handler thread otherwise there will be a race condition
+ * on mDataUsageRequests.
+ */
+ private void handleUnregister(DataUsageRequest request, int callingUid) {
+ RequestInfo requestInfo;
+ requestInfo = mDataUsageRequests.get(request.requestId);
+ if (requestInfo == null) {
+ if (LOGV) Slog.v(TAG, "Trying to unregister unknown request " + request);
+ return;
+ }
+ if (Process.SYSTEM_UID != callingUid && requestInfo.mCallingUid != callingUid) {
+ Slog.w(TAG, "Caller uid " + callingUid + " is not owner of " + request);
+ return;
+ }
+
+ if (LOGV) Slog.v(TAG, "Unregistering " + request);
+ mDataUsageRequests.remove(request.requestId);
+ requestInfo.unlinkDeathRecipient();
+ requestInfo.callCallback(NetworkStatsManager.CALLBACK_RELEASED);
+ }
+
+ private void handleUpdateStats(StatsContext statsContext) {
+ if (mDataUsageRequests.size() == 0) {
+ return;
+ }
+
+ for (int i = 0; i < mDataUsageRequests.size(); i++) {
+ RequestInfo requestInfo = mDataUsageRequests.valueAt(i);
+ requestInfo.updateStats(statsContext);
+ }
+ }
+
+ private DataUsageRequest buildRequest(DataUsageRequest request) {
+ // Cap the minimum threshold to a safe default to avoid too many callbacks
+ long thresholdInBytes = Math.max(MIN_THRESHOLD_BYTES, request.thresholdInBytes);
+ if (thresholdInBytes < request.thresholdInBytes) {
+ Slog.w(TAG, "Threshold was too low for " + request
+ + ". Overriding to a safer default of " + thresholdInBytes + " bytes");
+ }
+ return new DataUsageRequest(mNextDataUsageRequestId.incrementAndGet(),
+ request.template, thresholdInBytes);
+ }
+
+ private RequestInfo buildRequestInfo(DataUsageRequest request,
+ Messenger messenger, IBinder binder, int callingUid,
+ @NetworkStatsAccess.Level int accessLevel) {
+ if (accessLevel <= NetworkStatsAccess.Level.USER) {
+ return new UserUsageRequestInfo(this, request, messenger, binder, callingUid,
+ accessLevel);
+ } else {
+ // Safety check in case a new access level is added and we forgot to update this
+ checkArgument(accessLevel >= NetworkStatsAccess.Level.DEVICESUMMARY);
+ return new NetworkUsageRequestInfo(this, request, messenger, binder, callingUid,
+ accessLevel);
+ }
+ }
+
+ /**
+ * Tracks information relevant to a data usage observer.
+ * It will notice when the calling process dies so we can self-expire.
+ */
+ private abstract static class RequestInfo implements IBinder.DeathRecipient {
+ private final NetworkStatsObservers mStatsObserver;
+ protected final DataUsageRequest mRequest;
+ private final Messenger mMessenger;
+ private final IBinder mBinder;
+ protected final int mCallingUid;
+ protected final @NetworkStatsAccess.Level int mAccessLevel;
+ protected NetworkStatsRecorder mRecorder;
+ protected NetworkStatsCollection mCollection;
+
+ RequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
+ Messenger messenger, IBinder binder, int callingUid,
+ @NetworkStatsAccess.Level int accessLevel) {
+ mStatsObserver = statsObserver;
+ mRequest = request;
+ mMessenger = messenger;
+ mBinder = binder;
+ mCallingUid = callingUid;
+ mAccessLevel = accessLevel;
+
+ try {
+ mBinder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ binderDied();
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ if (LOGV) Slog.v(TAG, "RequestInfo binderDied("
+ + mRequest + ", " + mBinder + ")");
+ mStatsObserver.unregister(mRequest, Process.SYSTEM_UID);
+ callCallback(NetworkStatsManager.CALLBACK_RELEASED);
+ }
+
+ @Override
+ public String toString() {
+ return "RequestInfo from uid:" + mCallingUid
+ + " for " + mRequest + " accessLevel:" + mAccessLevel;
+ }
+
+ private void unlinkDeathRecipient() {
+ if (mBinder != null) {
+ mBinder.unlinkToDeath(this, 0);
+ }
+ }
+
+ /**
+ * Update stats given the samples and interface to identity mappings.
+ */
+ private void updateStats(StatsContext statsContext) {
+ if (mRecorder == null) {
+ // First run; establish baseline stats
+ resetRecorder();
+ recordSample(statsContext);
+ return;
+ }
+ recordSample(statsContext);
+
+ if (checkStats()) {
+ resetRecorder();
+ callCallback(NetworkStatsManager.CALLBACK_LIMIT_REACHED);
+ }
+ }
+
+ private void callCallback(int callbackType) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(DataUsageRequest.PARCELABLE_KEY, mRequest);
+ Message msg = Message.obtain();
+ msg.what = callbackType;
+ msg.setData(bundle);
+ try {
+ if (LOGV) {
+ Slog.v(TAG, "sending notification " + callbackTypeToName(callbackType)
+ + " for " + mRequest);
+ }
+ mMessenger.send(msg);
+ } catch (RemoteException e) {
+ // May occur naturally in the race of binder death.
+ Slog.w(TAG, "RemoteException caught trying to send a callback msg for " + mRequest);
+ }
+ }
+
+ private void resetRecorder() {
+ mRecorder = new NetworkStatsRecorder();
+ mCollection = mRecorder.getSinceBoot();
+ }
+
+ protected abstract boolean checkStats();
+
+ protected abstract void recordSample(StatsContext statsContext);
+
+ private String callbackTypeToName(int callbackType) {
+ switch (callbackType) {
+ case NetworkStatsManager.CALLBACK_LIMIT_REACHED:
+ return "LIMIT_REACHED";
+ case NetworkStatsManager.CALLBACK_RELEASED:
+ return "RELEASED";
+ default:
+ return "UNKNOWN";
+ }
+ }
+ }
+
+ private static class NetworkUsageRequestInfo extends RequestInfo {
+ NetworkUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
+ Messenger messenger, IBinder binder, int callingUid,
+ @NetworkStatsAccess.Level int accessLevel) {
+ super(statsObserver, request, messenger, binder, callingUid, accessLevel);
+ }
+
+ @Override
+ protected boolean checkStats() {
+ long bytesSoFar = getTotalBytesForNetwork(mRequest.template);
+ if (LOGV) {
+ Slog.v(TAG, bytesSoFar + " bytes so far since notification for "
+ + mRequest.template);
+ }
+ if (bytesSoFar > mRequest.thresholdInBytes) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void recordSample(StatsContext statsContext) {
+ // Recorder does not need to be locked in this context since only the handler
+ // thread will update it. We pass a null VPN array because usage is aggregated by uid
+ // for this snapshot, so VPN traffic can't be reattributed to responsible apps.
+ mRecorder.recordSnapshotLocked(statsContext.mXtSnapshot, statsContext.mActiveIfaces,
+ statsContext.mCurrentTime);
+ }
+
+ /**
+ * Reads stats matching the given template. {@link NetworkStatsCollection} will aggregate
+ * over all buckets, which in this case should be only one since we built it big enough
+ * that it will outlive the caller. If it doesn't, then there will be multiple buckets.
+ */
+ private long getTotalBytesForNetwork(NetworkTemplate template) {
+ NetworkStats stats = mCollection.getSummary(template,
+ Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
+ mAccessLevel, mCallingUid);
+ return stats.getTotalBytes();
+ }
+ }
+
+ private static class UserUsageRequestInfo extends RequestInfo {
+ UserUsageRequestInfo(NetworkStatsObservers statsObserver, DataUsageRequest request,
+ Messenger messenger, IBinder binder, int callingUid,
+ @NetworkStatsAccess.Level int accessLevel) {
+ super(statsObserver, request, messenger, binder, callingUid, accessLevel);
+ }
+
+ @Override
+ protected boolean checkStats() {
+ int[] uidsToMonitor = mCollection.getRelevantUids(mAccessLevel, mCallingUid);
+
+ for (int i = 0; i < uidsToMonitor.length; i++) {
+ long bytesSoFar = getTotalBytesForNetworkUid(mRequest.template, uidsToMonitor[i]);
+ if (bytesSoFar > mRequest.thresholdInBytes) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void recordSample(StatsContext statsContext) {
+ // Recorder does not need to be locked in this context since only the handler
+ // thread will update it. We pass the VPN info so VPN traffic is reattributed to
+ // responsible apps.
+ mRecorder.recordSnapshotLocked(statsContext.mUidSnapshot, statsContext.mActiveUidIfaces,
+ statsContext.mCurrentTime);
+ }
+
+ /**
+ * Reads all stats matching the given template and uid. Ther history will likely only
+ * contain one bucket per ident since we build it big enough that it will outlive the
+ * caller lifetime.
+ */
+ private long getTotalBytesForNetworkUid(NetworkTemplate template, int uid) {
+ try {
+ NetworkStatsHistory history = mCollection.getHistory(template, null, uid,
+ NetworkStats.SET_ALL, NetworkStats.TAG_NONE,
+ NetworkStatsHistory.FIELD_ALL,
+ Long.MIN_VALUE /* start */, Long.MAX_VALUE /* end */,
+ mAccessLevel, mCallingUid);
+ return history.getTotalBytes();
+ } catch (SecurityException e) {
+ if (LOGV) {
+ Slog.w(TAG, "CallerUid " + mCallingUid + " may have lost access to uid "
+ + uid);
+ }
+ return 0;
+ }
+ }
+ }
+
+ private static class StatsContext {
+ NetworkStats mXtSnapshot;
+ NetworkStats mUidSnapshot;
+ ArrayMap<String, NetworkIdentitySet> mActiveIfaces;
+ ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces;
+ long mCurrentTime;
+
+ StatsContext(NetworkStats xtSnapshot, NetworkStats uidSnapshot,
+ ArrayMap<String, NetworkIdentitySet> activeIfaces,
+ ArrayMap<String, NetworkIdentitySet> activeUidIfaces,
+ long currentTime) {
+ mXtSnapshot = xtSnapshot;
+ mUidSnapshot = uidSnapshot;
+ mActiveIfaces = activeIfaces;
+ mActiveUidIfaces = activeUidIfaces;
+ mCurrentTime = currentTime;
+ }
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
new file mode 100644
index 0000000..978ae87
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2012 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 static android.net.NetworkStats.TAG_NONE;
+import static android.net.TrafficStats.KB_IN_BYTES;
+import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+
+import android.net.NetworkStats;
+import android.net.NetworkStats.NonMonotonicObserver;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TrafficStats;
+import android.os.Binder;
+import android.os.DropBoxManager;
+import android.service.NetworkStatsRecorderProto;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.FileRotator;
+import com.android.internal.util.IndentingPrintWriter;
+
+import com.google.android.collect.Sets;
+
+import libcore.io.IoUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Logic to record deltas between periodic {@link NetworkStats} snapshots into
+ * {@link NetworkStatsHistory} that belong to {@link NetworkStatsCollection}.
+ * Keeps pending changes in memory until they pass a specific threshold, in
+ * bytes. Uses {@link FileRotator} for persistence logic if present.
+ * <p>
+ * Not inherently thread safe.
+ */
+public class NetworkStatsRecorder {
+ private static final String TAG = "NetworkStatsRecorder";
+ private static final boolean LOGD = false;
+ private static final boolean LOGV = false;
+
+ private static final String TAG_NETSTATS_DUMP = "netstats_dump";
+
+ /** Dump before deleting in {@link #recoverFromWtf()}. */
+ private static final boolean DUMP_BEFORE_DELETE = true;
+
+ private final FileRotator mRotator;
+ private final NonMonotonicObserver<String> mObserver;
+ private final DropBoxManager mDropBox;
+ private final String mCookie;
+
+ private final long mBucketDuration;
+ private final boolean mOnlyTags;
+
+ private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
+ private NetworkStats mLastSnapshot;
+
+ private final NetworkStatsCollection mPending;
+ private final NetworkStatsCollection mSinceBoot;
+
+ private final CombiningRewriter mPendingRewriter;
+
+ private WeakReference<NetworkStatsCollection> mComplete;
+
+ /**
+ * Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}.
+ */
+ public NetworkStatsRecorder() {
+ mRotator = null;
+ mObserver = null;
+ mDropBox = null;
+ mCookie = null;
+
+ // set the bucket big enough to have all data in one bucket, but allow some
+ // slack to avoid overflow
+ mBucketDuration = YEAR_IN_MILLIS;
+ mOnlyTags = false;
+
+ mPending = null;
+ mSinceBoot = new NetworkStatsCollection(mBucketDuration);
+
+ mPendingRewriter = null;
+ }
+
+ /**
+ * Persisted recorder.
+ */
+ public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
+ DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
+ mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
+ mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
+ mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
+ mCookie = cookie;
+
+ mBucketDuration = bucketDuration;
+ mOnlyTags = onlyTags;
+
+ mPending = new NetworkStatsCollection(bucketDuration);
+ mSinceBoot = new NetworkStatsCollection(bucketDuration);
+
+ mPendingRewriter = new CombiningRewriter(mPending);
+ }
+
+ public void setPersistThreshold(long thresholdBytes) {
+ if (LOGV) Slog.v(TAG, "setPersistThreshold() with " + thresholdBytes);
+ mPersistThresholdBytes = MathUtils.constrain(
+ thresholdBytes, 1 * KB_IN_BYTES, 100 * MB_IN_BYTES);
+ }
+
+ public void resetLocked() {
+ mLastSnapshot = null;
+ if (mPending != null) {
+ mPending.reset();
+ }
+ if (mSinceBoot != null) {
+ mSinceBoot.reset();
+ }
+ if (mComplete != null) {
+ mComplete.clear();
+ }
+ }
+
+ public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) {
+ return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE,
+ NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotal(null);
+ }
+
+ public NetworkStatsCollection getSinceBoot() {
+ return mSinceBoot;
+ }
+
+ /**
+ * Load complete history represented by {@link FileRotator}. Caches
+ * internally as a {@link WeakReference}, and updated with future
+ * {@link #recordSnapshotLocked(NetworkStats, Map, long)} snapshots as long
+ * as reference is valid.
+ */
+ public NetworkStatsCollection getOrLoadCompleteLocked() {
+ Objects.requireNonNull(mRotator, "missing FileRotator");
+ NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
+ if (res == null) {
+ res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+ mComplete = new WeakReference<NetworkStatsCollection>(res);
+ }
+ return res;
+ }
+
+ public NetworkStatsCollection getOrLoadPartialLocked(long start, long end) {
+ Objects.requireNonNull(mRotator, "missing FileRotator");
+ NetworkStatsCollection res = mComplete != null ? mComplete.get() : null;
+ if (res == null) {
+ res = loadLocked(start, end);
+ }
+ return res;
+ }
+
+ private NetworkStatsCollection loadLocked(long start, long end) {
+ if (LOGD) Slog.d(TAG, "loadLocked() reading from disk for " + mCookie);
+ final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration);
+ try {
+ mRotator.readMatching(res, start, end);
+ res.recordCollection(mPending);
+ } catch (IOException e) {
+ Log.wtf(TAG, "problem completely reading network stats", e);
+ recoverFromWtf();
+ } catch (OutOfMemoryError e) {
+ Log.wtf(TAG, "problem completely reading network stats", e);
+ recoverFromWtf();
+ }
+ return res;
+ }
+
+ /**
+ * Record any delta that occurred since last {@link NetworkStats} snapshot, using the given
+ * {@link Map} to identify network interfaces. First snapshot is considered bootstrap, and is
+ * not counted as delta.
+ */
+ public void recordSnapshotLocked(NetworkStats snapshot,
+ Map<String, NetworkIdentitySet> ifaceIdent, long currentTimeMillis) {
+ final HashSet<String> unknownIfaces = Sets.newHashSet();
+
+ // skip recording when snapshot missing
+ if (snapshot == null) return;
+
+ // assume first snapshot is bootstrap and don't record
+ if (mLastSnapshot == null) {
+ mLastSnapshot = snapshot;
+ return;
+ }
+
+ final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+
+ final NetworkStats delta = NetworkStats.subtract(
+ snapshot, mLastSnapshot, mObserver, mCookie);
+ final long end = currentTimeMillis;
+ final long start = end - delta.getElapsedRealtime();
+
+ NetworkStats.Entry entry = null;
+ for (int i = 0; i < delta.size(); i++) {
+ entry = delta.getValues(i, entry);
+
+ // As a last-ditch check, report any negative values and
+ // clamp them so recording below doesn't croak.
+ if (entry.isNegative()) {
+ if (mObserver != null) {
+ mObserver.foundNonMonotonic(delta, i, mCookie);
+ }
+ entry.rxBytes = Math.max(entry.rxBytes, 0);
+ entry.rxPackets = Math.max(entry.rxPackets, 0);
+ entry.txBytes = Math.max(entry.txBytes, 0);
+ entry.txPackets = Math.max(entry.txPackets, 0);
+ entry.operations = Math.max(entry.operations, 0);
+ }
+
+ final NetworkIdentitySet ident = ifaceIdent.get(entry.iface);
+ if (ident == null) {
+ unknownIfaces.add(entry.iface);
+ continue;
+ }
+
+ // skip when no delta occurred
+ if (entry.isEmpty()) continue;
+
+ // only record tag data when requested
+ if ((entry.tag == TAG_NONE) != mOnlyTags) {
+ if (mPending != null) {
+ mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+ }
+
+ // also record against boot stats when present
+ if (mSinceBoot != null) {
+ mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+ }
+
+ // also record against complete dataset when present
+ if (complete != null) {
+ complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
+ }
+ }
+ }
+
+ mLastSnapshot = snapshot;
+
+ if (LOGV && unknownIfaces.size() > 0) {
+ Slog.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats");
+ }
+ }
+
+ /**
+ * Consider persisting any pending deltas, if they are beyond
+ * {@link #mPersistThresholdBytes}.
+ */
+ public void maybePersistLocked(long currentTimeMillis) {
+ Objects.requireNonNull(mRotator, "missing FileRotator");
+ final long pendingBytes = mPending.getTotalBytes();
+ if (pendingBytes >= mPersistThresholdBytes) {
+ forcePersistLocked(currentTimeMillis);
+ } else {
+ mRotator.maybeRotate(currentTimeMillis);
+ }
+ }
+
+ /**
+ * Force persisting any pending deltas.
+ */
+ public void forcePersistLocked(long currentTimeMillis) {
+ Objects.requireNonNull(mRotator, "missing FileRotator");
+ if (mPending.isDirty()) {
+ if (LOGD) Slog.d(TAG, "forcePersistLocked() writing for " + mCookie);
+ try {
+ mRotator.rewriteActive(mPendingRewriter, currentTimeMillis);
+ mRotator.maybeRotate(currentTimeMillis);
+ mPending.reset();
+ } catch (IOException e) {
+ Log.wtf(TAG, "problem persisting pending stats", e);
+ recoverFromWtf();
+ } catch (OutOfMemoryError e) {
+ Log.wtf(TAG, "problem persisting pending stats", e);
+ recoverFromWtf();
+ }
+ }
+ }
+
+ /**
+ * Remove the given UID from all {@link FileRotator} history, migrating it
+ * to {@link TrafficStats#UID_REMOVED}.
+ */
+ public void removeUidsLocked(int[] uids) {
+ if (mRotator != null) {
+ try {
+ // Rewrite all persisted data to migrate UID stats
+ mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
+ } catch (IOException e) {
+ Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
+ recoverFromWtf();
+ } catch (OutOfMemoryError e) {
+ Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
+ recoverFromWtf();
+ }
+ }
+
+ // Remove any pending stats
+ if (mPending != null) {
+ mPending.removeUids(uids);
+ }
+ if (mSinceBoot != null) {
+ mSinceBoot.removeUids(uids);
+ }
+
+ // Clear UID from current stats snapshot
+ if (mLastSnapshot != null) {
+ mLastSnapshot.removeUids(uids);
+ }
+
+ final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
+ if (complete != null) {
+ complete.removeUids(uids);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ private static class CombiningRewriter implements FileRotator.Rewriter {
+ private final NetworkStatsCollection mCollection;
+
+ public CombiningRewriter(NetworkStatsCollection collection) {
+ mCollection = Objects.requireNonNull(collection, "missing NetworkStatsCollection");
+ }
+
+ @Override
+ public void reset() {
+ // ignored
+ }
+
+ @Override
+ public void read(InputStream in) throws IOException {
+ mCollection.read(in);
+ }
+
+ @Override
+ public boolean shouldWrite() {
+ return true;
+ }
+
+ @Override
+ public void write(OutputStream out) throws IOException {
+ mCollection.write(out);
+ mCollection.reset();
+ }
+ }
+
+ /**
+ * Rewriter that will remove any {@link NetworkStatsHistory} attributed to
+ * the requested UID, only writing data back when modified.
+ */
+ public static class RemoveUidRewriter implements FileRotator.Rewriter {
+ private final NetworkStatsCollection mTemp;
+ private final int[] mUids;
+
+ public RemoveUidRewriter(long bucketDuration, int[] uids) {
+ mTemp = new NetworkStatsCollection(bucketDuration);
+ mUids = uids;
+ }
+
+ @Override
+ public void reset() {
+ mTemp.reset();
+ }
+
+ @Override
+ public void read(InputStream in) throws IOException {
+ mTemp.read(in);
+ mTemp.clearDirty();
+ mTemp.removeUids(mUids);
+ }
+
+ @Override
+ public boolean shouldWrite() {
+ return mTemp.isDirty();
+ }
+
+ @Override
+ public void write(OutputStream out) throws IOException {
+ mTemp.write(out);
+ }
+ }
+
+ public void importLegacyNetworkLocked(File file) throws IOException {
+ Objects.requireNonNull(mRotator, "missing FileRotator");
+
+ // legacy file still exists; start empty to avoid double importing
+ mRotator.deleteAll();
+
+ final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
+ collection.readLegacyNetwork(file);
+
+ final long startMillis = collection.getStartMillis();
+ final long endMillis = collection.getEndMillis();
+
+ if (!collection.isEmpty()) {
+ // process legacy data, creating active file at starting time, then
+ // using end time to possibly trigger rotation.
+ mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
+ mRotator.maybeRotate(endMillis);
+ }
+ }
+
+ public void importLegacyUidLocked(File file) throws IOException {
+ Objects.requireNonNull(mRotator, "missing FileRotator");
+
+ // legacy file still exists; start empty to avoid double importing
+ mRotator.deleteAll();
+
+ final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
+ collection.readLegacyUid(file, mOnlyTags);
+
+ final long startMillis = collection.getStartMillis();
+ final long endMillis = collection.getEndMillis();
+
+ if (!collection.isEmpty()) {
+ // process legacy data, creating active file at starting time, then
+ // using end time to possibly trigger rotation.
+ mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
+ mRotator.maybeRotate(endMillis);
+ }
+ }
+
+ public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) {
+ if (mPending != null) {
+ pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes());
+ }
+ if (fullHistory) {
+ pw.println("Complete history:");
+ getOrLoadCompleteLocked().dump(pw);
+ } else {
+ pw.println("History since boot:");
+ mSinceBoot.dump(pw);
+ }
+ }
+
+ public void dumpDebugLocked(ProtoOutputStream proto, long tag) {
+ final long start = proto.start(tag);
+ if (mPending != null) {
+ proto.write(NetworkStatsRecorderProto.PENDING_TOTAL_BYTES, mPending.getTotalBytes());
+ }
+ getOrLoadCompleteLocked().dumpDebug(proto, NetworkStatsRecorderProto.COMPLETE_HISTORY);
+ proto.end(start);
+ }
+
+ public void dumpCheckin(PrintWriter pw, long start, long end) {
+ // Only load and dump stats from the requested window
+ getOrLoadPartialLocked(start, end).dumpCheckin(pw, start, end);
+ }
+
+ /**
+ * Recover from {@link FileRotator} failure by dumping state to
+ * {@link DropBoxManager} and deleting contents.
+ */
+ private void recoverFromWtf() {
+ if (DUMP_BEFORE_DELETE) {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try {
+ mRotator.dumpAll(os);
+ } catch (IOException e) {
+ // ignore partial contents
+ os.reset();
+ } finally {
+ IoUtils.closeQuietly(os);
+ }
+ mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
+ }
+
+ mRotator.deleteAll();
+ }
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
new file mode 100644
index 0000000..c876d41
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -0,0 +1,2253 @@
+/*
+ * Copyright (C) 2011 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 static android.Manifest.permission.NETWORK_STATS_PROVIDER;
+import static android.Manifest.permission.READ_NETWORK_USAGE_HISTORY;
+import static android.Manifest.permission.UPDATE_DEVICE_STATS;
+import static android.content.Intent.ACTION_SHUTDOWN;
+import static android.content.Intent.ACTION_UID_REMOVED;
+import static android.content.Intent.ACTION_USER_REMOVED;
+import static android.content.Intent.EXTRA_UID;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkIdentity.SUBTYPE_COMBINED;
+import static android.net.NetworkStack.checkNetworkStackPermission;
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.IFACE_VT;
+import static android.net.NetworkStats.INTERFACES_ALL;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.SET_ALL;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.SET_FOREGROUND;
+import static android.net.NetworkStats.STATS_PER_IFACE;
+import static android.net.NetworkStats.STATS_PER_UID;
+import static android.net.NetworkStats.TAG_ALL;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStatsHistory.FIELD_ALL;
+import static android.net.NetworkTemplate.buildTemplateMobileWildcard;
+import static android.net.NetworkTemplate.buildTemplateWifiWildcard;
+import static android.net.TetheringManager.ACTION_TETHER_STATE_CHANGED;
+import static android.net.TrafficStats.KB_IN_BYTES;
+import static android.net.TrafficStats.MB_IN_BYTES;
+import static android.net.TrafficStats.UNSUPPORTED;
+import static android.os.Trace.TRACE_TAG_NETWORK;
+import static android.provider.Settings.Global.NETSTATS_AUGMENT_ENABLED;
+import static android.provider.Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED;
+import static android.provider.Settings.Global.NETSTATS_DEV_BUCKET_DURATION;
+import static android.provider.Settings.Global.NETSTATS_DEV_DELETE_AGE;
+import static android.provider.Settings.Global.NETSTATS_DEV_PERSIST_BYTES;
+import static android.provider.Settings.Global.NETSTATS_DEV_ROTATE_AGE;
+import static android.provider.Settings.Global.NETSTATS_GLOBAL_ALERT_BYTES;
+import static android.provider.Settings.Global.NETSTATS_POLL_INTERVAL;
+import static android.provider.Settings.Global.NETSTATS_SAMPLE_ENABLED;
+import static android.provider.Settings.Global.NETSTATS_UID_BUCKET_DURATION;
+import static android.provider.Settings.Global.NETSTATS_UID_DELETE_AGE;
+import static android.provider.Settings.Global.NETSTATS_UID_PERSIST_BYTES;
+import static android.provider.Settings.Global.NETSTATS_UID_ROTATE_AGE;
+import static android.provider.Settings.Global.NETSTATS_UID_TAG_BUCKET_DURATION;
+import static android.provider.Settings.Global.NETSTATS_UID_TAG_DELETE_AGE;
+import static android.provider.Settings.Global.NETSTATS_UID_TAG_PERSIST_BYTES;
+import static android.provider.Settings.Global.NETSTATS_UID_TAG_ROTATE_AGE;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+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;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+import static com.android.net.module.util.NetworkCapabilitiesUtils.getDisplayTransport;
+import static com.android.server.NetworkManagementService.LIMIT_GLOBAL_ALERT;
+import static com.android.server.NetworkManagementSocketTagger.resetKernelUidStats;
+import static com.android.server.NetworkManagementSocketTagger.setKernelCounterSet;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.usage.NetworkStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.net.DataUsageRequest;
+import android.net.INetworkManagementEventObserver;
+import android.net.INetworkStatsService;
+import android.net.INetworkStatsSession;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
+import android.net.NetworkSpecifier;
+import android.net.NetworkStack;
+import android.net.NetworkStateSnapshot;
+import android.net.NetworkStats;
+import android.net.NetworkStats.NonMonotonicObserver;
+import android.net.NetworkStatsHistory;
+import android.net.NetworkTemplate;
+import android.net.TelephonyNetworkSpecifier;
+import android.net.TrafficStats;
+import android.net.UnderlyingNetworkInfo;
+import android.net.Uri;
+import android.net.netstats.provider.INetworkStatsProvider;
+import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.net.netstats.provider.NetworkStatsProvider;
+import android.os.BestClock;
+import android.os.Binder;
+import android.os.DropBoxManager;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.service.NetworkInterfaceProto;
+import android.service.NetworkStatsServiceDumpProto;
+import android.telephony.PhoneStateListener;
+import android.telephony.SubscriptionPlan;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FileRotator;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.net.module.util.BinderUtils;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.time.Clock;
+import java.time.ZoneOffset;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Collect and persist detailed network statistics, and provide this data to
+ * other system services.
+ */
+public class NetworkStatsService extends INetworkStatsService.Stub {
+ static final String TAG = "NetworkStats";
+ static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
+ static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
+
+ // Perform polling and persist all (FLAG_PERSIST_ALL).
+ private static final int MSG_PERFORM_POLL = 1;
+ // Perform polling, persist network, and register the global alert again.
+ private static final int MSG_PERFORM_POLL_REGISTER_ALERT = 2;
+ private static final int MSG_NOTIFY_NETWORK_STATUS = 3;
+ // A message for broadcasting ACTION_NETWORK_STATS_UPDATED in handler thread to prevent
+ // deadlock.
+ private static final int MSG_BROADCAST_NETWORK_STATS_UPDATED = 4;
+
+ /** Flags to control detail level of poll event. */
+ private static final int FLAG_PERSIST_NETWORK = 0x1;
+ private static final int FLAG_PERSIST_UID = 0x2;
+ private static final int FLAG_PERSIST_ALL = FLAG_PERSIST_NETWORK | FLAG_PERSIST_UID;
+ private static final int FLAG_PERSIST_FORCE = 0x100;
+
+ /**
+ * When global alert quota is high, wait for this delay before processing each polling,
+ * and do not schedule further polls once there is already one queued.
+ * This avoids firing the global alert too often on devices with high transfer speeds and
+ * high quota.
+ */
+ private static final int DEFAULT_PERFORM_POLL_DELAY_MS = 1000;
+
+ private static final String TAG_NETSTATS_ERROR = "netstats_error";
+
+ private final Context mContext;
+ private final INetworkManagementService mNetworkManager;
+ private final NetworkStatsFactory mStatsFactory;
+ private final AlarmManager mAlarmManager;
+ private final Clock mClock;
+ private final NetworkStatsSettings mSettings;
+ private final NetworkStatsObservers mStatsObservers;
+
+ private final File mSystemDir;
+ private final File mBaseDir;
+
+ private final PowerManager.WakeLock mWakeLock;
+
+ private final ContentObserver mContentObserver;
+ private final ContentResolver mContentResolver;
+
+ @VisibleForTesting
+ public static final String ACTION_NETWORK_STATS_POLL =
+ "com.android.server.action.NETWORK_STATS_POLL";
+ public static final String ACTION_NETWORK_STATS_UPDATED =
+ "com.android.server.action.NETWORK_STATS_UPDATED";
+
+ private PendingIntent mPollIntent;
+
+ private static final String PREFIX_DEV = "dev";
+ private static final String PREFIX_XT = "xt";
+ private static final String PREFIX_UID = "uid";
+ private static final String PREFIX_UID_TAG = "uid_tag";
+
+ /**
+ * Settings that can be changed externally.
+ */
+ public interface NetworkStatsSettings {
+ long getPollInterval();
+ long getPollDelay();
+ boolean getSampleEnabled();
+ boolean getAugmentEnabled();
+ /**
+ * When enabled, all mobile data is reported under {@link NetworkIdentity#SUBTYPE_COMBINED}.
+ * When disabled, mobile data is broken down by a granular subtype representative of the
+ * actual subtype. {@see NetworkTemplate#getCollapsedRatType}.
+ * Enabling this decreases the level of detail but saves performance, disk space and
+ * amount of data logged.
+ */
+ boolean getCombineSubtypeEnabled();
+
+ class Config {
+ public final long bucketDuration;
+ public final long rotateAgeMillis;
+ public final long deleteAgeMillis;
+
+ public Config(long bucketDuration, long rotateAgeMillis, long deleteAgeMillis) {
+ this.bucketDuration = bucketDuration;
+ this.rotateAgeMillis = rotateAgeMillis;
+ this.deleteAgeMillis = deleteAgeMillis;
+ }
+ }
+
+ Config getDevConfig();
+ Config getXtConfig();
+ Config getUidConfig();
+ Config getUidTagConfig();
+
+ long getGlobalAlertBytes(long def);
+ long getDevPersistBytes(long def);
+ long getXtPersistBytes(long def);
+ long getUidPersistBytes(long def);
+ long getUidTagPersistBytes(long def);
+ }
+
+ private final Object mStatsLock = new Object();
+
+ /** Set of currently active ifaces. */
+ @GuardedBy("mStatsLock")
+ private final ArrayMap<String, NetworkIdentitySet> mActiveIfaces = new ArrayMap<>();
+
+ /** Set of currently active ifaces for UID stats. */
+ @GuardedBy("mStatsLock")
+ private final ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces = new ArrayMap<>();
+
+ /** Current default active iface. */
+ @GuardedBy("mStatsLock")
+ private String mActiveIface;
+
+ /** Set of any ifaces associated with mobile networks since boot. */
+ private volatile String[] mMobileIfaces = new String[0];
+
+ /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */
+ @GuardedBy("mStatsLock")
+ private Network[] mDefaultNetworks = new Network[0];
+
+ /** Last states of all networks sent from ConnectivityService. */
+ @GuardedBy("mStatsLock")
+ @Nullable
+ private NetworkStateSnapshot[] mLastNetworkStateSnapshots = null;
+
+ private final DropBoxNonMonotonicObserver mNonMonotonicObserver =
+ new DropBoxNonMonotonicObserver();
+
+ private static final int MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS = 100;
+ private final CopyOnWriteArrayList<NetworkStatsProviderCallbackImpl> mStatsProviderCbList =
+ new CopyOnWriteArrayList<>();
+ /** Semaphore used to wait for stats provider to respond to request stats update. */
+ private final Semaphore mStatsProviderSem = new Semaphore(0, true);
+
+ @GuardedBy("mStatsLock")
+ private NetworkStatsRecorder mDevRecorder;
+ @GuardedBy("mStatsLock")
+ private NetworkStatsRecorder mXtRecorder;
+ @GuardedBy("mStatsLock")
+ private NetworkStatsRecorder mUidRecorder;
+ @GuardedBy("mStatsLock")
+ private NetworkStatsRecorder mUidTagRecorder;
+
+ /** Cached {@link #mXtRecorder} stats. */
+ @GuardedBy("mStatsLock")
+ private NetworkStatsCollection mXtStatsCached;
+
+ /** Current counter sets for each UID. */
+ private SparseIntArray mActiveUidCounterSet = new SparseIntArray();
+
+ /** Data layer operation counters for splicing into other structures. */
+ private NetworkStats mUidOperations = new NetworkStats(0L, 10);
+
+ @NonNull
+ private final Handler mHandler;
+
+ private volatile boolean mSystemReady;
+ private long mPersistThreshold = 2 * MB_IN_BYTES;
+ private long mGlobalAlertBytes;
+
+ private static final long POLL_RATE_LIMIT_MS = 15_000;
+
+ private long mLastStatsSessionPoll;
+
+ /** Map from UID to number of opened sessions */
+ @GuardedBy("mOpenSessionCallsPerUid")
+ private final SparseIntArray mOpenSessionCallsPerUid = new SparseIntArray();
+
+ private final static int DUMP_STATS_SESSION_COUNT = 20;
+
+ @NonNull
+ private final Dependencies mDeps;
+
+ @NonNull
+ private final NetworkStatsSubscriptionsMonitor mNetworkStatsSubscriptionsMonitor;
+
+ 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());
+ }
+
+ private final class NetworkStatsHandler extends Handler {
+ NetworkStatsHandler(@NonNull Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_PERFORM_POLL: {
+ performPoll(FLAG_PERSIST_ALL);
+ break;
+ }
+ case MSG_NOTIFY_NETWORK_STATUS: {
+ // If no cached states, ignore.
+ if (mLastNetworkStateSnapshots == null) break;
+ // TODO (b/181642673): Protect mDefaultNetworks from concurrent accessing.
+ handleNotifyNetworkStatus(
+ mDefaultNetworks, mLastNetworkStateSnapshots, mActiveIface);
+ break;
+ }
+ case MSG_PERFORM_POLL_REGISTER_ALERT: {
+ performPoll(FLAG_PERSIST_NETWORK);
+ registerGlobalAlert();
+ break;
+ }
+ case MSG_BROADCAST_NETWORK_STATS_UPDATED: {
+ final Intent updatedIntent = new Intent(ACTION_NETWORK_STATS_UPDATED);
+ updatedIntent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ mContext.sendBroadcastAsUser(updatedIntent, UserHandle.ALL,
+ READ_NETWORK_USAGE_HISTORY);
+ break;
+ }
+ }
+ }
+ }
+
+ public static NetworkStatsService create(Context context,
+ INetworkManagementService networkManager) {
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock wakeLock =
+ powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+
+ final NetworkStatsService service = new NetworkStatsService(context, networkManager,
+ alarmManager, wakeLock, getDefaultClock(),
+ new DefaultNetworkStatsSettings(context), new NetworkStatsFactory(),
+ new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(),
+ new Dependencies());
+ service.registerLocalService();
+
+ return service;
+ }
+
+ // This must not be called outside of tests, even within the same package, as this constructor
+ // does not register the local service. Use the create() helper above.
+ @VisibleForTesting
+ NetworkStatsService(Context context, INetworkManagementService networkManager,
+ AlarmManager alarmManager, PowerManager.WakeLock wakeLock, Clock clock,
+ NetworkStatsSettings settings, NetworkStatsFactory factory,
+ NetworkStatsObservers statsObservers, File systemDir, File baseDir,
+ @NonNull Dependencies deps) {
+ mContext = Objects.requireNonNull(context, "missing Context");
+ mNetworkManager = Objects.requireNonNull(networkManager,
+ "missing INetworkManagementService");
+ mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
+ mClock = Objects.requireNonNull(clock, "missing Clock");
+ mSettings = Objects.requireNonNull(settings, "missing NetworkStatsSettings");
+ 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");
+
+ final HandlerThread handlerThread = mDeps.makeHandlerThread();
+ handlerThread.start();
+ mHandler = new NetworkStatsHandler(handlerThread.getLooper());
+ mNetworkStatsSubscriptionsMonitor = deps.makeSubscriptionsMonitor(mContext,
+ mHandler.getLooper(), new HandlerExecutor(mHandler), this);
+ mContentResolver = mContext.getContentResolver();
+ mContentObserver = mDeps.makeContentObserver(mHandler, mSettings,
+ mNetworkStatsSubscriptionsMonitor);
+ }
+
+ /**
+ * Dependencies of NetworkStatsService, for injection in tests.
+ */
+ // TODO: Move more stuff into dependencies object.
+ @VisibleForTesting
+ public static class Dependencies {
+ /**
+ * Create a HandlerThread to use in NetworkStatsService.
+ */
+ @NonNull
+ public HandlerThread makeHandlerThread() {
+ return new HandlerThread(TAG);
+ }
+
+ /**
+ * Create a {@link NetworkStatsSubscriptionsMonitor}, can be used to monitor RAT change
+ * event in NetworkStatsService.
+ */
+ @NonNull
+ public NetworkStatsSubscriptionsMonitor makeSubscriptionsMonitor(@NonNull Context context,
+ @NonNull Looper looper, @NonNull Executor executor,
+ @NonNull NetworkStatsService service) {
+ // TODO: Update RatType passively in NSS, instead of querying into the monitor
+ // when notifyNetworkStatus.
+ return new NetworkStatsSubscriptionsMonitor(context, looper, executor,
+ (subscriberId, type) -> service.handleOnCollapsedRatTypeChanged());
+ }
+
+ /**
+ * Create a ContentObserver instance which is used to observe settings changes,
+ * and dispatch onChange events on handler thread.
+ */
+ public @NonNull ContentObserver makeContentObserver(@NonNull Handler handler,
+ @NonNull NetworkStatsSettings settings,
+ @NonNull NetworkStatsSubscriptionsMonitor monitor) {
+ return new ContentObserver(handler) {
+ @Override
+ public void onChange(boolean selfChange, @NonNull Uri uri) {
+ if (!settings.getCombineSubtypeEnabled()) {
+ monitor.start();
+ } else {
+ monitor.stop();
+ }
+ }
+ };
+ }
+ }
+
+ private void registerLocalService() {
+ LocalServices.addService(NetworkStatsManagerInternal.class,
+ new NetworkStatsManagerInternalImpl());
+ }
+
+ public void systemReady() {
+ synchronized (mStatsLock) {
+ 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);
+
+ updatePersistThresholdsLocked();
+
+ // upgrade any legacy stats, migrating them to rotated files
+ maybeUpgradeLegacyStatsLocked();
+
+ // read historical network stats from disk, since policy service
+ // might need them right away.
+ mXtStatsCached = mXtRecorder.getOrLoadCompleteLocked();
+
+ // bootstrap initial stats to prevent double-counting later
+ bootstrapStatsLocked();
+ }
+
+ // watch for tethering changes
+ final IntentFilter tetherFilter = new IntentFilter(ACTION_TETHER_STATE_CHANGED);
+ mContext.registerReceiver(mTetherReceiver, tetherFilter, null, mHandler);
+
+ // listen for periodic polling events
+ final IntentFilter pollFilter = new IntentFilter(ACTION_NETWORK_STATS_POLL);
+ mContext.registerReceiver(mPollReceiver, pollFilter, READ_NETWORK_USAGE_HISTORY, mHandler);
+
+ // listen for uid removal to clean stats
+ final IntentFilter removedFilter = new IntentFilter(ACTION_UID_REMOVED);
+ mContext.registerReceiver(mRemovedReceiver, removedFilter, null, mHandler);
+
+ // listen for user changes to clean stats
+ final IntentFilter userFilter = new IntentFilter(ACTION_USER_REMOVED);
+ mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler);
+
+ // persist stats during clean shutdown
+ final IntentFilter shutdownFilter = new IntentFilter(ACTION_SHUTDOWN);
+ mContext.registerReceiver(mShutdownReceiver, shutdownFilter);
+
+ try {
+ mNetworkManager.registerObserver(mAlertObserver);
+ } catch (RemoteException e) {
+ // ignored; service lives in system_server
+ }
+
+ // schedule periodic pall alarm based on {@link NetworkStatsSettings#getPollInterval()}.
+ final PendingIntent pollIntent =
+ PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_NETWORK_STATS_POLL),
+ PendingIntent.FLAG_IMMUTABLE);
+
+ final long currentRealtime = SystemClock.elapsedRealtime();
+ mAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, currentRealtime,
+ mSettings.getPollInterval(), pollIntent);
+
+ mContentResolver.registerContentObserver(Settings.Global
+ .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED),
+ false /* notifyForDescendants */, mContentObserver);
+
+ // Post a runnable on handler thread to call onChange(). It's for getting current value of
+ // NETSTATS_COMBINE_SUBTYPE_ENABLED to decide start or stop monitoring RAT type changes.
+ mHandler.post(() -> mContentObserver.onChange(false, Settings.Global
+ .getUriFor(Settings.Global.NETSTATS_COMBINE_SUBTYPE_ENABLED)));
+
+ registerGlobalAlert();
+ }
+
+ private NetworkStatsRecorder buildRecorder(
+ String prefix, NetworkStatsSettings.Config config, boolean includeTags) {
+ final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
+ Context.DROPBOX_SERVICE);
+ return new NetworkStatsRecorder(new FileRotator(
+ mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
+ mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
+ }
+
+ @GuardedBy("mStatsLock")
+ private void shutdownLocked() {
+ mContext.unregisterReceiver(mTetherReceiver);
+ mContext.unregisterReceiver(mPollReceiver);
+ mContext.unregisterReceiver(mRemovedReceiver);
+ mContext.unregisterReceiver(mUserReceiver);
+ mContext.unregisterReceiver(mShutdownReceiver);
+
+ if (!mSettings.getCombineSubtypeEnabled()) {
+ mNetworkStatsSubscriptionsMonitor.stop();
+ }
+
+ mContentResolver.unregisterContentObserver(mContentObserver);
+
+ final long currentTime = mClock.millis();
+
+ // persist any pending stats
+ mDevRecorder.forcePersistLocked(currentTime);
+ mXtRecorder.forcePersistLocked(currentTime);
+ mUidRecorder.forcePersistLocked(currentTime);
+ mUidTagRecorder.forcePersistLocked(currentTime);
+
+ mSystemReady = 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);
+ }
+ }
+
+ /**
+ * Register for a global alert that is delivered through {@link INetworkManagementEventObserver}
+ * or {@link NetworkStatsProviderCallback#onAlertReached()} once a threshold amount of data has
+ * been transferred.
+ */
+ private void registerGlobalAlert() {
+ try {
+ mNetworkManager.setGlobalAlert(mGlobalAlertBytes);
+ } catch (IllegalStateException e) {
+ Slog.w(TAG, "problem registering for global alert: " + e);
+ } catch (RemoteException e) {
+ // ignored; service lives in system_server
+ }
+ invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetAlert(mGlobalAlertBytes));
+ }
+
+ @Override
+ public INetworkStatsSession openSession() {
+ // NOTE: if callers want to get non-augmented data, they should go
+ // through the public API
+ return openSessionInternal(NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, null);
+ }
+
+ @Override
+ public INetworkStatsSession openSessionForUsageStats(int flags, String callingPackage) {
+ return openSessionInternal(flags, callingPackage);
+ }
+
+ private boolean isRateLimitedForPoll(int callingUid) {
+ if (callingUid == android.os.Process.SYSTEM_UID) {
+ return false;
+ }
+
+ final long lastCallTime;
+ final long now = SystemClock.elapsedRealtime();
+ synchronized (mOpenSessionCallsPerUid) {
+ int calls = mOpenSessionCallsPerUid.get(callingUid, 0);
+ mOpenSessionCallsPerUid.put(callingUid, calls + 1);
+ lastCallTime = mLastStatsSessionPoll;
+ mLastStatsSessionPoll = now;
+ }
+
+ return now - lastCallTime < POLL_RATE_LIMIT_MS;
+ }
+
+ private INetworkStatsSession openSessionInternal(final int flags, final String callingPackage) {
+ final int callingUid = Binder.getCallingUid();
+ final int usedFlags = isRateLimitedForPoll(callingUid)
+ ? flags & (~NetworkStatsManager.FLAG_POLL_ON_OPEN)
+ : flags;
+ if ((usedFlags & (NetworkStatsManager.FLAG_POLL_ON_OPEN
+ | NetworkStatsManager.FLAG_POLL_FORCE)) != 0) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ performPoll(FLAG_PERSIST_ALL);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ // return an IBinder which holds strong references to any loaded stats
+ // for its lifetime; when caller closes only weak references remain.
+
+ return new INetworkStatsSession.Stub() {
+ private final int mCallingUid = callingUid;
+ private final String mCallingPackage = callingPackage;
+ private final @NetworkStatsAccess.Level int mAccessLevel = checkAccessLevel(
+ callingPackage);
+
+ private NetworkStatsCollection mUidComplete;
+ private NetworkStatsCollection mUidTagComplete;
+
+ private NetworkStatsCollection getUidComplete() {
+ synchronized (mStatsLock) {
+ if (mUidComplete == null) {
+ mUidComplete = mUidRecorder.getOrLoadCompleteLocked();
+ }
+ return mUidComplete;
+ }
+ }
+
+ private NetworkStatsCollection getUidTagComplete() {
+ synchronized (mStatsLock) {
+ if (mUidTagComplete == null) {
+ mUidTagComplete = mUidTagRecorder.getOrLoadCompleteLocked();
+ }
+ return mUidTagComplete;
+ }
+ }
+
+ @Override
+ public int[] getRelevantUids() {
+ return getUidComplete().getRelevantUids(mAccessLevel);
+ }
+
+ @Override
+ public NetworkStats getDeviceSummaryForNetwork(
+ NetworkTemplate template, long start, long end) {
+ return internalGetSummaryForNetwork(template, usedFlags, start, end, mAccessLevel,
+ mCallingUid);
+ }
+
+ @Override
+ public NetworkStats getSummaryForNetwork(
+ NetworkTemplate template, long start, long end) {
+ return internalGetSummaryForNetwork(template, usedFlags, start, end, mAccessLevel,
+ mCallingUid);
+ }
+
+ @Override
+ public NetworkStatsHistory getHistoryForNetwork(NetworkTemplate template, int fields) {
+ return internalGetHistoryForNetwork(template, usedFlags, fields, mAccessLevel,
+ mCallingUid);
+ }
+
+ @Override
+ public NetworkStats getSummaryForAllUid(
+ NetworkTemplate template, long start, long end, boolean includeTags) {
+ try {
+ final NetworkStats stats = getUidComplete()
+ .getSummary(template, start, end, mAccessLevel, mCallingUid);
+ if (includeTags) {
+ final NetworkStats tagStats = getUidTagComplete()
+ .getSummary(template, start, end, mAccessLevel, mCallingUid);
+ stats.combineAllValues(tagStats);
+ }
+ return stats;
+ } catch (NullPointerException e) {
+ // TODO: Track down and fix the cause of this crash and remove this catch block.
+ Slog.wtf(TAG, "NullPointerException in getSummaryForAllUid", e);
+ throw e;
+ }
+ }
+
+ @Override
+ public NetworkStatsHistory getHistoryForUid(
+ NetworkTemplate template, int uid, int set, int tag, int fields) {
+ // NOTE: We don't augment UID-level statistics
+ if (tag == TAG_NONE) {
+ return getUidComplete().getHistory(template, null, uid, set, tag, fields,
+ Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid);
+ } else {
+ return getUidTagComplete().getHistory(template, null, uid, set, tag, fields,
+ Long.MIN_VALUE, Long.MAX_VALUE, mAccessLevel, mCallingUid);
+ }
+ }
+
+ @Override
+ public NetworkStatsHistory getHistoryIntervalForUid(
+ NetworkTemplate template, int uid, int set, int tag, int fields,
+ long start, long end) {
+ // NOTE: We don't augment UID-level statistics
+ if (tag == TAG_NONE) {
+ return getUidComplete().getHistory(template, null, uid, set, tag, fields,
+ start, end, mAccessLevel, mCallingUid);
+ } else if (uid == Binder.getCallingUid()) {
+ return getUidTagComplete().getHistory(template, null, uid, set, tag, fields,
+ start, end, mAccessLevel, mCallingUid);
+ } else {
+ throw new SecurityException("Calling package " + mCallingPackage
+ + " cannot access tag information from a different uid");
+ }
+ }
+
+ @Override
+ public void close() {
+ mUidComplete = null;
+ mUidTagComplete = null;
+ }
+ };
+ }
+
+ private @NetworkStatsAccess.Level int checkAccessLevel(String callingPackage) {
+ return NetworkStatsAccess.checkAccessLevel(
+ mContext, Binder.getCallingUid(), callingPackage);
+ }
+
+ /**
+ * Find the most relevant {@link SubscriptionPlan} for the given
+ * {@link NetworkTemplate} and flags. This is typically used to augment
+ * local measurement results to match a known anchor from the carrier.
+ */
+ private SubscriptionPlan resolveSubscriptionPlan(NetworkTemplate template, int flags) {
+ SubscriptionPlan plan = null;
+ if ((flags & NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN) != 0
+ && mSettings.getAugmentEnabled()) {
+ if (LOGD) Slog.d(TAG, "Resolving plan for " + template);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ plan = LocalServices.getService(NetworkPolicyManagerInternal.class)
+ .getSubscriptionPlan(template);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ if (LOGD) Slog.d(TAG, "Resolved to plan " + plan);
+ }
+ return plan;
+ }
+
+ /**
+ * Return network summary, splicing between DEV and XT stats when
+ * appropriate.
+ */
+ private NetworkStats internalGetSummaryForNetwork(NetworkTemplate template, int flags,
+ long start, long end, @NetworkStatsAccess.Level int accessLevel, int callingUid) {
+ // We've been using pure XT stats long enough that we no longer need to
+ // splice DEV and XT together.
+ final NetworkStatsHistory history = internalGetHistoryForNetwork(template, flags, FIELD_ALL,
+ accessLevel, callingUid);
+
+ final long now = System.currentTimeMillis();
+ final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
+
+ final NetworkStats stats = new NetworkStats(end - start, 1);
+ stats.insertEntry(new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE,
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, entry.rxBytes, entry.rxPackets,
+ entry.txBytes, entry.txPackets, entry.operations));
+ return stats;
+ }
+
+ /**
+ * Return network history, splicing between DEV and XT stats when
+ * appropriate.
+ */
+ private NetworkStatsHistory internalGetHistoryForNetwork(NetworkTemplate template,
+ int flags, int fields, @NetworkStatsAccess.Level int accessLevel, int callingUid) {
+ // We've been using pure XT stats long enough that we no longer need to
+ // splice DEV and XT together.
+ final SubscriptionPlan augmentPlan = resolveSubscriptionPlan(template, flags);
+ synchronized (mStatsLock) {
+ return mXtStatsCached.getHistory(template, augmentPlan,
+ UID_ALL, SET_ALL, TAG_NONE, fields, Long.MIN_VALUE, Long.MAX_VALUE,
+ accessLevel, callingUid);
+ }
+ }
+
+ private long getNetworkTotalBytes(NetworkTemplate template, long start, long end) {
+ assertSystemReady();
+
+ // NOTE: if callers want to get non-augmented data, they should go
+ // through the public API
+ return internalGetSummaryForNetwork(template,
+ NetworkStatsManager.FLAG_AUGMENT_WITH_SUBSCRIPTION_PLAN, start, end,
+ NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotalBytes();
+ }
+
+ private NetworkStats getNetworkUidBytes(NetworkTemplate template, long start, long end) {
+ assertSystemReady();
+
+ final NetworkStatsCollection uidComplete;
+ synchronized (mStatsLock) {
+ uidComplete = mUidRecorder.getOrLoadCompleteLocked();
+ }
+ return uidComplete.getSummary(template, start, end, NetworkStatsAccess.Level.DEVICE,
+ android.os.Process.SYSTEM_UID);
+ }
+
+ @Override
+ public NetworkStats getDataLayerSnapshotForUid(int uid) throws RemoteException {
+ if (Binder.getCallingUid() != uid) {
+ Log.w(TAG, "Snapshots only available for calling UID");
+ return new NetworkStats(SystemClock.elapsedRealtime(), 0);
+ }
+
+ // TODO: switch to data layer stats once kernel exports
+ // for now, read network layer stats and flatten across all ifaces
+ final NetworkStats networkLayer = readNetworkStatsUidDetail(uid, INTERFACES_ALL, TAG_ALL);
+
+ // splice in operation counts
+ networkLayer.spliceOperationsFrom(mUidOperations);
+
+ final NetworkStats dataLayer = new NetworkStats(
+ networkLayer.getElapsedRealtime(), networkLayer.size());
+
+ NetworkStats.Entry entry = null;
+ for (int i = 0; i < networkLayer.size(); i++) {
+ entry = networkLayer.getValues(i, entry);
+ entry.iface = IFACE_ALL;
+ dataLayer.combineValues(entry);
+ }
+
+ return dataLayer;
+ }
+
+ @Override
+ public NetworkStats getDetailedUidStats(String[] requiredIfaces) {
+ try {
+ final String[] ifacesToQuery =
+ mStatsFactory.augmentWithStackedInterfaces(requiredIfaces);
+ return getNetworkStatsUidDetail(ifacesToQuery);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Error compiling UID stats", e);
+ return new NetworkStats(0L, 0);
+ }
+ }
+
+ @Override
+ public String[] getMobileIfaces() {
+ // TODO (b/192758557): Remove debug log.
+ if (ArrayUtils.contains(mMobileIfaces, null)) {
+ throw new NullPointerException(
+ "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
+ }
+ return mMobileIfaces.clone();
+ }
+
+ @Override
+ public void incrementOperationCount(int uid, int tag, int operationCount) {
+ if (Binder.getCallingUid() != uid) {
+ mContext.enforceCallingOrSelfPermission(UPDATE_DEVICE_STATS, TAG);
+ }
+
+ if (operationCount < 0) {
+ throw new IllegalArgumentException("operation count can only be incremented");
+ }
+ if (tag == TAG_NONE) {
+ throw new IllegalArgumentException("operation count must have specific tag");
+ }
+
+ synchronized (mStatsLock) {
+ final int set = mActiveUidCounterSet.get(uid, SET_DEFAULT);
+ mUidOperations.combineValues(
+ mActiveIface, uid, set, tag, 0L, 0L, 0L, 0L, operationCount);
+ mUidOperations.combineValues(
+ mActiveIface, uid, set, TAG_NONE, 0L, 0L, 0L, 0L, operationCount);
+ }
+ }
+
+ @VisibleForTesting
+ void setUidForeground(int uid, boolean uidForeground) {
+ synchronized (mStatsLock) {
+ final int set = uidForeground ? SET_FOREGROUND : SET_DEFAULT;
+ final int oldSet = mActiveUidCounterSet.get(uid, SET_DEFAULT);
+ if (oldSet != set) {
+ mActiveUidCounterSet.put(uid, set);
+ setKernelCounterSet(uid, set);
+ }
+ }
+ }
+
+ /**
+ * Notify {@code NetworkStatsService} about network status changed.
+ */
+ public void notifyNetworkStatus(
+ @NonNull Network[] defaultNetworks,
+ @NonNull NetworkStateSnapshot[] networkStates,
+ @Nullable String activeIface,
+ @NonNull UnderlyingNetworkInfo[] underlyingNetworkInfos) {
+ checkNetworkStackPermission(mContext);
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ handleNotifyNetworkStatus(defaultNetworks, networkStates, activeIface);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ // Update the VPN underlying interfaces only after the poll is made and tun data has been
+ // migrated. Otherwise the migration would use the new interfaces instead of the ones that
+ // were current when the polled data was transferred.
+ mStatsFactory.updateUnderlyingNetworkInfos(underlyingNetworkInfos);
+ }
+
+ @Override
+ public void forceUpdate() {
+ mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ performPoll(FLAG_PERSIST_ALL);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ private void advisePersistThreshold(long thresholdBytes) {
+ // clamp threshold into safe range
+ mPersistThreshold = MathUtils.constrain(thresholdBytes, 128 * KB_IN_BYTES, 2 * MB_IN_BYTES);
+ if (LOGV) {
+ Slog.v(TAG, "advisePersistThreshold() given " + thresholdBytes + ", clamped to "
+ + mPersistThreshold);
+ }
+
+ final long oldGlobalAlertBytes = mGlobalAlertBytes;
+
+ // update and persist if beyond new thresholds
+ final long currentTime = mClock.millis();
+ synchronized (mStatsLock) {
+ if (!mSystemReady) return;
+
+ updatePersistThresholdsLocked();
+
+ mDevRecorder.maybePersistLocked(currentTime);
+ mXtRecorder.maybePersistLocked(currentTime);
+ mUidRecorder.maybePersistLocked(currentTime);
+ mUidTagRecorder.maybePersistLocked(currentTime);
+ }
+
+ if (oldGlobalAlertBytes != mGlobalAlertBytes) {
+ registerGlobalAlert();
+ }
+ }
+
+ @Override
+ public DataUsageRequest registerUsageCallback(String callingPackage,
+ DataUsageRequest request, Messenger messenger, IBinder binder) {
+ Objects.requireNonNull(callingPackage, "calling package is null");
+ Objects.requireNonNull(request, "DataUsageRequest is null");
+ Objects.requireNonNull(request.template, "NetworkTemplate is null");
+ Objects.requireNonNull(messenger, "messenger is null");
+ Objects.requireNonNull(binder, "binder is null");
+
+ int callingUid = Binder.getCallingUid();
+ @NetworkStatsAccess.Level int accessLevel = checkAccessLevel(callingPackage);
+ DataUsageRequest normalizedRequest;
+ final long token = Binder.clearCallingIdentity();
+ try {
+ normalizedRequest = mStatsObservers.register(request, messenger, binder,
+ callingUid, accessLevel);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ // Create baseline stats
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_PERFORM_POLL));
+
+ return normalizedRequest;
+ }
+
+ @Override
+ public void unregisterUsageRequest(DataUsageRequest request) {
+ Objects.requireNonNull(request, "DataUsageRequest is null");
+
+ int callingUid = Binder.getCallingUid();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mStatsObservers.unregister(request, callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public long getUidStats(int uid, int type) {
+ final int callingUid = Binder.getCallingUid();
+ if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
+ return UNSUPPORTED;
+ }
+ return nativeGetUidStat(uid, type);
+ }
+
+ @Override
+ public long getIfaceStats(@NonNull String iface, int type) {
+ Objects.requireNonNull(iface);
+ long nativeIfaceStats = nativeGetIfaceStat(iface, type);
+ if (nativeIfaceStats == -1) {
+ return nativeIfaceStats;
+ } else {
+ // When tethering offload is in use, nativeIfaceStats does not contain usage from
+ // offload, add it back here. Note that the included statistics might be stale
+ // since polling newest stats from hardware might impact system health and not
+ // suitable for TrafficStats API use cases.
+ return nativeIfaceStats + getProviderIfaceStats(iface, type);
+ }
+ }
+
+ @Override
+ public long getTotalStats(int type) {
+ long nativeTotalStats = nativeGetTotalStat(type);
+ if (nativeTotalStats == -1) {
+ return nativeTotalStats;
+ } else {
+ // Refer to comment in getIfaceStats
+ return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type);
+ }
+ }
+
+ private long getProviderIfaceStats(@Nullable String iface, int type) {
+ final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
+ final HashSet<String> limitIfaces;
+ if (iface == IFACE_ALL) {
+ limitIfaces = null;
+ } else {
+ limitIfaces = new HashSet<>();
+ limitIfaces.add(iface);
+ }
+ final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces);
+ switch (type) {
+ case TrafficStats.TYPE_RX_BYTES:
+ return entry.rxBytes;
+ case TrafficStats.TYPE_RX_PACKETS:
+ return entry.rxPackets;
+ case TrafficStats.TYPE_TX_BYTES:
+ return entry.txBytes;
+ case TrafficStats.TYPE_TX_PACKETS:
+ return entry.txPackets;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Update {@link NetworkStatsRecorder} and {@link #mGlobalAlertBytes} to
+ * reflect current {@link #mPersistThreshold} value. Always defers to
+ * {@link Global} values when defined.
+ */
+ @GuardedBy("mStatsLock")
+ private void updatePersistThresholdsLocked() {
+ mDevRecorder.setPersistThreshold(mSettings.getDevPersistBytes(mPersistThreshold));
+ mXtRecorder.setPersistThreshold(mSettings.getXtPersistBytes(mPersistThreshold));
+ mUidRecorder.setPersistThreshold(mSettings.getUidPersistBytes(mPersistThreshold));
+ mUidTagRecorder.setPersistThreshold(mSettings.getUidTagPersistBytes(mPersistThreshold));
+ mGlobalAlertBytes = mSettings.getGlobalAlertBytes(mPersistThreshold);
+ }
+
+ /**
+ * Receiver that watches for {@link Tethering} to claim interface pairs.
+ */
+ private BroadcastReceiver mTetherReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ performPoll(FLAG_PERSIST_NETWORK);
+ }
+ };
+
+ private BroadcastReceiver mPollReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // on background handler thread, and verified UPDATE_DEVICE_STATS
+ // permission above.
+ performPoll(FLAG_PERSIST_ALL);
+
+ // verify that we're watching global alert
+ registerGlobalAlert();
+ }
+ };
+
+ private BroadcastReceiver mRemovedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // on background handler thread, and UID_REMOVED is protected
+ // broadcast.
+
+ final int uid = intent.getIntExtra(EXTRA_UID, -1);
+ if (uid == -1) return;
+
+ synchronized (mStatsLock) {
+ mWakeLock.acquire();
+ try {
+ removeUidsLocked(uid);
+ } finally {
+ mWakeLock.release();
+ }
+ }
+ }
+ };
+
+ private BroadcastReceiver mUserReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // On background handler thread, and USER_REMOVED is protected
+ // broadcast.
+
+ final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+ if (userId == -1) return;
+
+ synchronized (mStatsLock) {
+ mWakeLock.acquire();
+ try {
+ removeUserLocked(userId);
+ } finally {
+ mWakeLock.release();
+ }
+ }
+ }
+ };
+
+ private BroadcastReceiver mShutdownReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // SHUTDOWN is protected broadcast.
+ synchronized (mStatsLock) {
+ shutdownLocked();
+ }
+ }
+ };
+
+ /**
+ * Observer that watches for {@link INetworkManagementService} alerts.
+ */
+ private final INetworkManagementEventObserver mAlertObserver = new BaseNetworkObserver() {
+ @Override
+ public void limitReached(String limitName, String iface) {
+ // only someone like NMS should be calling us
+ NetworkStack.checkNetworkStackPermission(mContext);
+
+ if (LIMIT_GLOBAL_ALERT.equals(limitName)) {
+ // kick off background poll to collect network stats unless there is already
+ // such a call pending; UID stats are handled during normal polling interval.
+ if (!mHandler.hasMessages(MSG_PERFORM_POLL_REGISTER_ALERT)) {
+ mHandler.sendEmptyMessageDelayed(MSG_PERFORM_POLL_REGISTER_ALERT,
+ mSettings.getPollDelay());
+ }
+ }
+ }
+ };
+
+ /**
+ * Handle collapsed RAT type changed event.
+ */
+ @VisibleForTesting
+ public void handleOnCollapsedRatTypeChanged() {
+ // Protect service from frequently updating. Remove pending messages if any.
+ mHandler.removeMessages(MSG_NOTIFY_NETWORK_STATUS);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_NOTIFY_NETWORK_STATUS), mSettings.getPollDelay());
+ }
+
+ private void handleNotifyNetworkStatus(
+ Network[] defaultNetworks,
+ NetworkStateSnapshot[] snapshots,
+ String activeIface) {
+ synchronized (mStatsLock) {
+ mWakeLock.acquire();
+ try {
+ mActiveIface = activeIface;
+ handleNotifyNetworkStatusLocked(defaultNetworks, snapshots);
+ } finally {
+ mWakeLock.release();
+ }
+ }
+ }
+
+ /**
+ * Inspect all current {@link NetworkStateSnapshot}s to derive mapping from {@code iface} to
+ * {@link NetworkStatsHistory}. When multiple networks are active on a single {@code iface},
+ * they are combined under a single {@link NetworkIdentitySet}.
+ */
+ @GuardedBy("mStatsLock")
+ private void handleNotifyNetworkStatusLocked(@NonNull Network[] defaultNetworks,
+ @NonNull NetworkStateSnapshot[] snapshots) {
+ if (!mSystemReady) return;
+ if (LOGV) Slog.v(TAG, "handleNotifyNetworkStatusLocked()");
+
+ // take one last stats snapshot before updating iface mapping. this
+ // isn't perfect, since the kernel may already be counting traffic from
+ // the updated network.
+
+ // poll, but only persist network stats to keep codepath fast. UID stats
+ // will be persisted during next alarm poll event.
+ performPollLocked(FLAG_PERSIST_NETWORK);
+
+ // Rebuild active interfaces based on connected networks
+ mActiveIfaces.clear();
+ mActiveUidIfaces.clear();
+ // Update the list of default networks.
+ mDefaultNetworks = defaultNetworks;
+
+ mLastNetworkStateSnapshots = snapshots;
+
+ final boolean combineSubtypeEnabled = mSettings.getCombineSubtypeEnabled();
+ final ArraySet<String> mobileIfaces = new ArraySet<>();
+ for (NetworkStateSnapshot snapshot : snapshots) {
+ final int displayTransport =
+ getDisplayTransport(snapshot.getNetworkCapabilities().getTransportTypes());
+ final boolean isMobile = (NetworkCapabilities.TRANSPORT_CELLULAR == displayTransport);
+ final boolean isDefault = ArrayUtils.contains(mDefaultNetworks, snapshot.getNetwork());
+ final int subType = combineSubtypeEnabled ? SUBTYPE_COMBINED
+ : getSubTypeForStateSnapshot(snapshot);
+ final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, snapshot,
+ isDefault, subType);
+
+ // Traffic occurring on the base interface is always counted for
+ // both total usage and UID details.
+ final String baseIface = snapshot.getLinkProperties().getInterfaceName();
+ if (baseIface != null) {
+ findOrCreateNetworkIdentitySet(mActiveIfaces, baseIface).add(ident);
+ findOrCreateNetworkIdentitySet(mActiveUidIfaces, baseIface).add(ident);
+
+ // Build a separate virtual interface for VT (Video Telephony) data usage.
+ // Only do this when IMS is not metered, but VT is metered.
+ // If IMS is metered, then the IMS network usage has already included VT usage.
+ // VT is considered always metered in framework's layer. If VT is not metered
+ // per carrier's policy, modem will report 0 usage for VT calls.
+ if (snapshot.getNetworkCapabilities().hasCapability(
+ NetworkCapabilities.NET_CAPABILITY_IMS) && !ident.getMetered()) {
+
+ // Copy the identify from IMS one but mark it as metered.
+ NetworkIdentity vtIdent = new NetworkIdentity(ident.getType(),
+ ident.getSubType(), ident.getSubscriberId(), ident.getNetworkId(),
+ ident.getRoaming(), true /* metered */,
+ true /* onDefaultNetwork */, ident.getOemManaged());
+ final String ifaceVt = IFACE_VT + getSubIdForMobile(snapshot);
+ findOrCreateNetworkIdentitySet(mActiveIfaces, ifaceVt).add(vtIdent);
+ findOrCreateNetworkIdentitySet(mActiveUidIfaces, ifaceVt).add(vtIdent);
+ }
+
+ if (isMobile) {
+ mobileIfaces.add(baseIface);
+ }
+ }
+
+ // Traffic occurring on stacked interfaces is usually clatd.
+ //
+ // UID stats are always counted on the stacked interface and never on the base
+ // interface, because the packets on the base interface do not actually match
+ // application sockets (they're not IPv4) and thus the app uid is not known.
+ // For receive this is obvious: packets must be translated from IPv6 to IPv4
+ // before the application socket can be found.
+ // For transmit: either they go through the clat daemon which by virtue of going
+ // through userspace strips the original socket association during the IPv4 to
+ // IPv6 translation process, or they are offloaded by eBPF, which doesn't:
+ // However, on an ebpf device the accounting is done in cgroup ebpf hooks,
+ // which don't trigger again post ebpf translation.
+ // (as such stats accounted to the clat uid are ignored)
+ //
+ // Interface stats are more complicated.
+ //
+ // eBPF offloaded 464xlat'ed packets never hit base interface ip6tables, and thus
+ // *all* statistics are collected by iptables on the stacked v4-* interface.
+ //
+ // Additionally for ingress all packets bound for the clat IPv6 address are dropped
+ // in ip6tables raw prerouting and thus even non-offloaded packets are only
+ // accounted for on the stacked interface.
+ //
+ // For egress, packets subject to eBPF offload never appear on the base interface
+ // and only appear on the stacked interface. Thus to ensure packets increment
+ // interface stats, we must collate data from stacked interfaces. For xt_qtaguid
+ // (or non eBPF offloaded) TX they would appear on both, however egress interface
+ // accounting is explicitly bypassed for traffic from the clat uid.
+ //
+ // TODO: This code might be combined to above code.
+ for (String iface : snapshot.getLinkProperties().getAllInterfaceNames()) {
+ // baseIface has been handled, so ignore it.
+ if (TextUtils.equals(baseIface, iface)) continue;
+ if (iface != null) {
+ findOrCreateNetworkIdentitySet(mActiveIfaces, iface).add(ident);
+ findOrCreateNetworkIdentitySet(mActiveUidIfaces, iface).add(ident);
+ if (isMobile) {
+ mobileIfaces.add(iface);
+ }
+
+ mStatsFactory.noteStackedIface(iface, baseIface);
+ }
+ }
+ }
+
+ mMobileIfaces = mobileIfaces.toArray(new String[0]);
+ // TODO (b/192758557): Remove debug log.
+ if (ArrayUtils.contains(mMobileIfaces, null)) {
+ throw new NullPointerException(
+ "null element in mMobileIfaces: " + Arrays.toString(mMobileIfaces));
+ }
+ }
+
+ private static int getSubIdForMobile(@NonNull NetworkStateSnapshot state) {
+ if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ throw new IllegalArgumentException("Mobile state need capability TRANSPORT_CELLULAR");
+ }
+
+ final NetworkSpecifier spec = state.getNetworkCapabilities().getNetworkSpecifier();
+ if (spec instanceof TelephonyNetworkSpecifier) {
+ return ((TelephonyNetworkSpecifier) spec).getSubscriptionId();
+ } else {
+ Slog.wtf(TAG, "getSubIdForState invalid NetworkSpecifier");
+ return INVALID_SUBSCRIPTION_ID;
+ }
+ }
+
+ /**
+ * For networks with {@code TRANSPORT_CELLULAR}, get subType that was obtained through
+ * {@link PhoneStateListener}. Otherwise, return 0 given that other networks with different
+ * transport types do not actually fill this value.
+ */
+ private int getSubTypeForStateSnapshot(@NonNull NetworkStateSnapshot state) {
+ if (!state.getNetworkCapabilities().hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+ return 0;
+ }
+
+ return mNetworkStatsSubscriptionsMonitor.getRatTypeForSubscriberId(state.getSubscriberId());
+ }
+
+ private static <K> NetworkIdentitySet findOrCreateNetworkIdentitySet(
+ ArrayMap<K, NetworkIdentitySet> map, K key) {
+ NetworkIdentitySet ident = map.get(key);
+ if (ident == null) {
+ ident = new NetworkIdentitySet();
+ map.put(key, ident);
+ }
+ return ident;
+ }
+
+ @GuardedBy("mStatsLock")
+ private void recordSnapshotLocked(long currentTime) throws RemoteException {
+ // snapshot and record current counters; read UID stats first to
+ // avoid over counting dev stats.
+ Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotUid");
+ final NetworkStats uidSnapshot = getNetworkStatsUidDetail(INTERFACES_ALL);
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotXt");
+ final NetworkStats xtSnapshot = readNetworkStatsSummaryXt();
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotDev");
+ final NetworkStats devSnapshot = readNetworkStatsSummaryDev();
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+
+ // Snapshot for dev/xt stats from all custom stats providers. Counts per-interface data
+ // from stats providers that isn't already counted by dev and XT stats.
+ Trace.traceBegin(TRACE_TAG_NETWORK, "snapshotStatsProvider");
+ final NetworkStats providersnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ xtSnapshot.combineAllValues(providersnapshot);
+ devSnapshot.combineAllValues(providersnapshot);
+
+ // For xt/dev, we pass a null VPN array because usage is aggregated by UID, so VPN traffic
+ // can't be reattributed to responsible apps.
+ Trace.traceBegin(TRACE_TAG_NETWORK, "recordDev");
+ mDevRecorder.recordSnapshotLocked(devSnapshot, mActiveIfaces, currentTime);
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ Trace.traceBegin(TRACE_TAG_NETWORK, "recordXt");
+ mXtRecorder.recordSnapshotLocked(xtSnapshot, mActiveIfaces, currentTime);
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+
+ // For per-UID stats, pass the VPN info so VPN traffic is reattributed to responsible apps.
+ Trace.traceBegin(TRACE_TAG_NETWORK, "recordUid");
+ mUidRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime);
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ Trace.traceBegin(TRACE_TAG_NETWORK, "recordUidTag");
+ mUidTagRecorder.recordSnapshotLocked(uidSnapshot, mActiveUidIfaces, currentTime);
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+
+ // We need to make copies of member fields that are sent to the observer to avoid
+ // a race condition between the service handler thread and the observer's
+ mStatsObservers.updateStats(xtSnapshot, uidSnapshot, new ArrayMap<>(mActiveIfaces),
+ new ArrayMap<>(mActiveUidIfaces), currentTime);
+ }
+
+ /**
+ * Bootstrap initial stats snapshot, usually during {@link #systemReady()}
+ * so we have baseline values without double-counting.
+ */
+ @GuardedBy("mStatsLock")
+ private void bootstrapStatsLocked() {
+ final long currentTime = mClock.millis();
+
+ try {
+ recordSnapshotLocked(currentTime);
+ } catch (IllegalStateException e) {
+ Slog.w(TAG, "problem reading network stats: " + e);
+ } catch (RemoteException e) {
+ // ignored; service lives in system_server
+ }
+ }
+
+ private void performPoll(int flags) {
+ synchronized (mStatsLock) {
+ mWakeLock.acquire();
+
+ try {
+ performPollLocked(flags);
+ } finally {
+ mWakeLock.release();
+ }
+ }
+ }
+
+ /**
+ * Periodic poll operation, reading current statistics and recording into
+ * {@link NetworkStatsHistory}.
+ */
+ @GuardedBy("mStatsLock")
+ private void performPollLocked(int flags) {
+ if (!mSystemReady) return;
+ if (LOGV) Slog.v(TAG, "performPollLocked(flags=0x" + Integer.toHexString(flags) + ")");
+ Trace.traceBegin(TRACE_TAG_NETWORK, "performPollLocked");
+
+ final boolean persistNetwork = (flags & FLAG_PERSIST_NETWORK) != 0;
+ final boolean persistUid = (flags & FLAG_PERSIST_UID) != 0;
+ final boolean persistForce = (flags & FLAG_PERSIST_FORCE) != 0;
+
+ performPollFromProvidersLocked();
+
+ // TODO: consider marking "untrusted" times in historical stats
+ final long currentTime = mClock.millis();
+
+ try {
+ recordSnapshotLocked(currentTime);
+ } catch (IllegalStateException e) {
+ Log.wtf(TAG, "problem reading network stats", e);
+ return;
+ } catch (RemoteException e) {
+ // ignored; service lives in system_server
+ return;
+ }
+
+ // persist any pending data depending on requested flags
+ Trace.traceBegin(TRACE_TAG_NETWORK, "[persisting]");
+ if (persistForce) {
+ mDevRecorder.forcePersistLocked(currentTime);
+ mXtRecorder.forcePersistLocked(currentTime);
+ mUidRecorder.forcePersistLocked(currentTime);
+ mUidTagRecorder.forcePersistLocked(currentTime);
+ } else {
+ if (persistNetwork) {
+ mDevRecorder.maybePersistLocked(currentTime);
+ mXtRecorder.maybePersistLocked(currentTime);
+ }
+ if (persistUid) {
+ mUidRecorder.maybePersistLocked(currentTime);
+ mUidTagRecorder.maybePersistLocked(currentTime);
+ }
+ }
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+
+ if (mSettings.getSampleEnabled()) {
+ // sample stats after each full poll
+ performSampleLocked();
+ }
+
+ // finally, dispatch updated event to any listeners
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_BROADCAST_NETWORK_STATS_UPDATED));
+
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ }
+
+ @GuardedBy("mStatsLock")
+ private void performPollFromProvidersLocked() {
+ // Request asynchronous stats update from all providers for next poll. And wait a bit of
+ // time to allow providers report-in given that normally binder call should be fast. Note
+ // that size of list might be changed because addition/removing at the same time. For
+ // addition, the stats of the missed provider can only be collected in next poll;
+ // for removal, wait might take up to MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS
+ // once that happened.
+ // TODO: request with a valid token.
+ Trace.traceBegin(TRACE_TAG_NETWORK, "provider.requestStatsUpdate");
+ final int registeredCallbackCount = mStatsProviderCbList.size();
+ mStatsProviderSem.drainPermits();
+ invokeForAllStatsProviderCallbacks(
+ (cb) -> cb.mProvider.onRequestStatsUpdate(0 /* unused */));
+ try {
+ mStatsProviderSem.tryAcquire(registeredCallbackCount,
+ MAX_STATS_PROVIDER_POLL_WAIT_TIME_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // Strictly speaking it's possible a provider happened to deliver between the timeout
+ // and the log, and that doesn't matter too much as this is just a debug log.
+ Log.d(TAG, "requestStatsUpdate - providers responded "
+ + mStatsProviderSem.availablePermits()
+ + "/" + registeredCallbackCount + " : " + e);
+ }
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ }
+
+ /**
+ * Sample recent statistics summary into {@link EventLog}.
+ */
+ @GuardedBy("mStatsLock")
+ private void performSampleLocked() {
+ // TODO: migrate trustedtime fixes to separate binary log events
+ final long currentTime = mClock.millis();
+
+ NetworkTemplate template;
+ NetworkStats.Entry devTotal;
+ NetworkStats.Entry xtTotal;
+ NetworkStats.Entry uidTotal;
+
+ // collect mobile sample
+ template = buildTemplateMobileWildcard();
+ devTotal = mDevRecorder.getTotalSinceBootLocked(template);
+ xtTotal = mXtRecorder.getTotalSinceBootLocked(template);
+ uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
+
+ EventLogTags.writeNetstatsMobileSample(
+ devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets,
+ xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+ uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+ currentTime);
+
+ // collect wifi sample
+ template = buildTemplateWifiWildcard();
+ devTotal = mDevRecorder.getTotalSinceBootLocked(template);
+ xtTotal = mXtRecorder.getTotalSinceBootLocked(template);
+ uidTotal = mUidRecorder.getTotalSinceBootLocked(template);
+
+ EventLogTags.writeNetstatsWifiSample(
+ devTotal.rxBytes, devTotal.rxPackets, devTotal.txBytes, devTotal.txPackets,
+ xtTotal.rxBytes, xtTotal.rxPackets, xtTotal.txBytes, xtTotal.txPackets,
+ uidTotal.rxBytes, uidTotal.rxPackets, uidTotal.txBytes, uidTotal.txPackets,
+ currentTime);
+ }
+
+ /**
+ * Clean up {@link #mUidRecorder} after UID is removed.
+ */
+ @GuardedBy("mStatsLock")
+ private void removeUidsLocked(int... uids) {
+ if (LOGV) Slog.v(TAG, "removeUidsLocked() for UIDs " + Arrays.toString(uids));
+
+ // Perform one last poll before removing
+ performPollLocked(FLAG_PERSIST_ALL);
+
+ mUidRecorder.removeUidsLocked(uids);
+ mUidTagRecorder.removeUidsLocked(uids);
+
+ // Clear kernel stats associated with UID
+ for (int uid : uids) {
+ resetKernelUidStats(uid);
+ }
+ }
+
+ /**
+ * Clean up {@link #mUidRecorder} after user is removed.
+ */
+ @GuardedBy("mStatsLock")
+ private void removeUserLocked(int userId) {
+ if (LOGV) Slog.v(TAG, "removeUserLocked() for userId=" + userId);
+
+ // Build list of UIDs that we should clean up
+ int[] uids = new int[0];
+ final List<ApplicationInfo> apps = mContext.getPackageManager().getInstalledApplications(
+ PackageManager.MATCH_ANY_USER
+ | PackageManager.MATCH_DISABLED_COMPONENTS);
+ for (ApplicationInfo app : apps) {
+ final int uid = UserHandle.getUid(userId, app.uid);
+ uids = ArrayUtils.appendInt(uids, uid);
+ }
+
+ removeUidsLocked(uids);
+ }
+
+ private class NetworkStatsManagerInternalImpl extends NetworkStatsManagerInternal {
+ @Override
+ public long getNetworkTotalBytes(NetworkTemplate template, long start, long end) {
+ Trace.traceBegin(TRACE_TAG_NETWORK, "getNetworkTotalBytes");
+ try {
+ return NetworkStatsService.this.getNetworkTotalBytes(template, start, end);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ }
+ }
+
+ @Override
+ public NetworkStats getNetworkUidBytes(NetworkTemplate template, long start, long end) {
+ Trace.traceBegin(TRACE_TAG_NETWORK, "getNetworkUidBytes");
+ try {
+ return NetworkStatsService.this.getNetworkUidBytes(template, start, end);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ }
+ }
+
+ @Override
+ public void setUidForeground(int uid, boolean uidForeground) {
+ NetworkStatsService.this.setUidForeground(uid, uidForeground);
+ }
+
+ @Override
+ public void advisePersistThreshold(long thresholdBytes) {
+ NetworkStatsService.this.advisePersistThreshold(thresholdBytes);
+ }
+
+ @Override
+ public void forceUpdate() {
+ NetworkStatsService.this.forceUpdate();
+ }
+
+ @Override
+ public void setStatsProviderWarningAndLimitAsync(
+ @NonNull String iface, long warning, long limit) {
+ if (LOGV) {
+ Slog.v(TAG, "setStatsProviderWarningAndLimitAsync("
+ + iface + "," + warning + "," + limit + ")");
+ }
+ invokeForAllStatsProviderCallbacks((cb) -> cb.mProvider.onSetWarningAndLimit(iface,
+ warning, limit));
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter rawWriter, String[] args) {
+ if (!DumpUtils.checkDumpPermission(mContext, TAG, rawWriter)) return;
+
+ long duration = DateUtils.DAY_IN_MILLIS;
+ final HashSet<String> argSet = new HashSet<String>();
+ for (String arg : args) {
+ argSet.add(arg);
+
+ if (arg.startsWith("--duration=")) {
+ try {
+ duration = Long.parseLong(arg.substring(11));
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ }
+
+ // usage: dumpsys netstats --full --uid --tag --poll --checkin
+ final boolean poll = argSet.contains("--poll") || argSet.contains("poll");
+ final boolean checkin = argSet.contains("--checkin");
+ final boolean fullHistory = argSet.contains("--full") || argSet.contains("full");
+ final boolean includeUid = argSet.contains("--uid") || argSet.contains("detail");
+ final boolean includeTag = argSet.contains("--tag") || argSet.contains("detail");
+
+ final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, " ");
+
+ synchronized (mStatsLock) {
+ if (args.length > 0 && "--proto".equals(args[0])) {
+ // In this case ignore all other arguments.
+ dumpProtoLocked(fd);
+ return;
+ }
+
+ if (poll) {
+ performPollLocked(FLAG_PERSIST_ALL | FLAG_PERSIST_FORCE);
+ pw.println("Forced poll");
+ return;
+ }
+
+ if (checkin) {
+ final long end = System.currentTimeMillis();
+ final long start = end - duration;
+
+ pw.print("v1,");
+ pw.print(start / SECOND_IN_MILLIS); pw.print(',');
+ pw.print(end / SECOND_IN_MILLIS); pw.println();
+
+ pw.println("xt");
+ mXtRecorder.dumpCheckin(rawWriter, start, end);
+
+ if (includeUid) {
+ pw.println("uid");
+ mUidRecorder.dumpCheckin(rawWriter, start, end);
+ }
+ if (includeTag) {
+ pw.println("tag");
+ mUidTagRecorder.dumpCheckin(rawWriter, start, end);
+ }
+ return;
+ }
+
+ pw.println("Configs:");
+ pw.increaseIndent();
+ pw.printPair(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
+ pw.println();
+ pw.decreaseIndent();
+
+ pw.println("Active interfaces:");
+ pw.increaseIndent();
+ for (int i = 0; i < mActiveIfaces.size(); i++) {
+ pw.printPair("iface", mActiveIfaces.keyAt(i));
+ pw.printPair("ident", mActiveIfaces.valueAt(i));
+ pw.println();
+ }
+ pw.decreaseIndent();
+
+ pw.println("Active UID interfaces:");
+ pw.increaseIndent();
+ for (int i = 0; i < mActiveUidIfaces.size(); i++) {
+ pw.printPair("iface", mActiveUidIfaces.keyAt(i));
+ pw.printPair("ident", mActiveUidIfaces.valueAt(i));
+ pw.println();
+ }
+ pw.decreaseIndent();
+
+ // Get the top openSession callers
+ final SparseIntArray calls;
+ synchronized (mOpenSessionCallsPerUid) {
+ calls = mOpenSessionCallsPerUid.clone();
+ }
+
+ final int N = calls.size();
+ final long[] values = new long[N];
+ for (int j = 0; j < N; j++) {
+ values[j] = ((long) calls.valueAt(j) << 32) | calls.keyAt(j);
+ }
+ Arrays.sort(values);
+
+ pw.println("Top openSession callers (uid=count):");
+ pw.increaseIndent();
+ final int end = Math.max(0, N - DUMP_STATS_SESSION_COUNT);
+ for (int j = N - 1; j >= end; j--) {
+ final int uid = (int) (values[j] & 0xffffffff);
+ final int count = (int) (values[j] >> 32);
+ pw.print(uid); pw.print("="); pw.println(count);
+ }
+ pw.decreaseIndent();
+ pw.println();
+
+ pw.println("Stats Providers:");
+ pw.increaseIndent();
+ invokeForAllStatsProviderCallbacks((cb) -> {
+ pw.println(cb.mTag + " Xt:");
+ pw.increaseIndent();
+ pw.print(cb.getCachedStats(STATS_PER_IFACE).toString());
+ pw.decreaseIndent();
+ if (includeUid) {
+ pw.println(cb.mTag + " Uid:");
+ pw.increaseIndent();
+ pw.print(cb.getCachedStats(STATS_PER_UID).toString());
+ pw.decreaseIndent();
+ }
+ });
+ pw.decreaseIndent();
+
+ pw.println("Dev stats:");
+ pw.increaseIndent();
+ mDevRecorder.dumpLocked(pw, fullHistory);
+ pw.decreaseIndent();
+
+ pw.println("Xt stats:");
+ pw.increaseIndent();
+ mXtRecorder.dumpLocked(pw, fullHistory);
+ pw.decreaseIndent();
+
+ if (includeUid) {
+ pw.println("UID stats:");
+ pw.increaseIndent();
+ mUidRecorder.dumpLocked(pw, fullHistory);
+ pw.decreaseIndent();
+ }
+
+ if (includeTag) {
+ pw.println("UID tag stats:");
+ pw.increaseIndent();
+ mUidTagRecorder.dumpLocked(pw, fullHistory);
+ pw.decreaseIndent();
+ }
+ }
+ }
+
+ @GuardedBy("mStatsLock")
+ private void dumpProtoLocked(FileDescriptor fd) {
+ final ProtoOutputStream proto = new ProtoOutputStream(fd);
+
+ // TODO Right now it writes all history. Should it limit to the "since-boot" log?
+
+ dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_INTERFACES, mActiveIfaces);
+ dumpInterfaces(proto, NetworkStatsServiceDumpProto.ACTIVE_UID_INTERFACES, mActiveUidIfaces);
+ mDevRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.DEV_STATS);
+ mXtRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.XT_STATS);
+ mUidRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.UID_STATS);
+ mUidTagRecorder.dumpDebugLocked(proto, NetworkStatsServiceDumpProto.UID_TAG_STATS);
+
+ proto.flush();
+ }
+
+ private static void dumpInterfaces(ProtoOutputStream proto, long tag,
+ ArrayMap<String, NetworkIdentitySet> ifaces) {
+ for (int i = 0; i < ifaces.size(); i++) {
+ final long start = proto.start(tag);
+
+ proto.write(NetworkInterfaceProto.INTERFACE, ifaces.keyAt(i));
+ ifaces.valueAt(i).dumpDebug(proto, NetworkInterfaceProto.IDENTITIES);
+
+ proto.end(start);
+ }
+ }
+
+ private NetworkStats readNetworkStatsSummaryDev() {
+ try {
+ return mStatsFactory.readNetworkStatsSummaryDev();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private NetworkStats readNetworkStatsSummaryXt() {
+ try {
+ return mStatsFactory.readNetworkStatsSummaryXt();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private NetworkStats readNetworkStatsUidDetail(int uid, String[] ifaces, int tag) {
+ try {
+ return mStatsFactory.readNetworkStatsDetail(uid, ifaces, tag);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * Return snapshot of current UID statistics, including any
+ * {@link TrafficStats#UID_TETHERING}, video calling data usage, and {@link #mUidOperations}
+ * values.
+ *
+ * @param ifaces A list of interfaces the stats should be restricted to, or
+ * {@link NetworkStats#INTERFACES_ALL}.
+ */
+ private NetworkStats getNetworkStatsUidDetail(String[] ifaces)
+ throws RemoteException {
+ final NetworkStats uidSnapshot = readNetworkStatsUidDetail(UID_ALL, ifaces, TAG_ALL);
+
+ // fold tethering stats and operations into uid snapshot
+ final NetworkStats tetherSnapshot = getNetworkStatsTethering(STATS_PER_UID);
+ tetherSnapshot.filter(UID_ALL, ifaces, TAG_ALL);
+ mStatsFactory.apply464xlatAdjustments(uidSnapshot, tetherSnapshot);
+ uidSnapshot.combineAllValues(tetherSnapshot);
+
+ // get a stale copy of uid stats snapshot provided by providers.
+ final NetworkStats providerStats = getNetworkStatsFromProviders(STATS_PER_UID);
+ providerStats.filter(UID_ALL, ifaces, TAG_ALL);
+ mStatsFactory.apply464xlatAdjustments(uidSnapshot, providerStats);
+ uidSnapshot.combineAllValues(providerStats);
+
+ uidSnapshot.combineAllValues(mUidOperations);
+
+ return uidSnapshot;
+ }
+
+ /**
+ * Return snapshot of current non-offloaded tethering statistics. Will return empty
+ * {@link NetworkStats} if any problems are encountered, or queried by {@code STATS_PER_IFACE}
+ * since it is already included by {@link #nativeGetIfaceStat}.
+ * See {@code OffloadTetheringStatsProvider} for offloaded tethering stats.
+ */
+ // TODO: Remove this by implementing {@link NetworkStatsProvider} for non-offloaded
+ // tethering stats.
+ private NetworkStats getNetworkStatsTethering(int how) throws RemoteException {
+ try {
+ return mNetworkManager.getNetworkStatsTethering(how);
+ } catch (IllegalStateException e) {
+ Log.wtf(TAG, "problem reading network stats", e);
+ return new NetworkStats(0L, 10);
+ }
+ }
+
+ // TODO: It is copied from ConnectivityService, consider refactor these check permission
+ // functions to a proper util.
+ private boolean checkAnyPermissionOf(String... permissions) {
+ for (String permission : permissions) {
+ if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void enforceAnyPermissionOf(String... permissions) {
+ if (!checkAnyPermissionOf(permissions)) {
+ throw new SecurityException("Requires one of the following permissions: "
+ + String.join(", ", permissions) + ".");
+ }
+ }
+
+ /**
+ * Registers a custom provider of {@link android.net.NetworkStats} to combine the network
+ * statistics that cannot be seen by the kernel to system. To unregister, invoke the
+ * {@code unregister()} of the returned callback.
+ *
+ * @param tag a human readable identifier of the custom network stats provider.
+ * @param provider the {@link INetworkStatsProvider} binder corresponding to the
+ * {@link NetworkStatsProvider} to be registered.
+ *
+ * @return a {@link INetworkStatsProviderCallback} binder
+ * interface, which can be used to report events to the system.
+ */
+ public @NonNull INetworkStatsProviderCallback registerNetworkStatsProvider(
+ @NonNull String tag, @NonNull INetworkStatsProvider provider) {
+ enforceAnyPermissionOf(NETWORK_STATS_PROVIDER,
+ NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+ Objects.requireNonNull(provider, "provider is null");
+ Objects.requireNonNull(tag, "tag is null");
+ try {
+ NetworkStatsProviderCallbackImpl callback = new NetworkStatsProviderCallbackImpl(
+ tag, provider, mStatsProviderSem, mAlertObserver,
+ mStatsProviderCbList);
+ mStatsProviderCbList.add(callback);
+ Log.d(TAG, "registerNetworkStatsProvider from " + callback.mTag + " uid/pid="
+ + getCallingUid() + "/" + getCallingPid());
+ return callback;
+ } catch (RemoteException e) {
+ Log.e(TAG, "registerNetworkStatsProvider failed", e);
+ }
+ return null;
+ }
+
+ // Collect stats from local cache of providers.
+ private @NonNull NetworkStats getNetworkStatsFromProviders(int how) {
+ final NetworkStats ret = new NetworkStats(0L, 0);
+ invokeForAllStatsProviderCallbacks((cb) -> ret.combineAllValues(cb.getCachedStats(how)));
+ return ret;
+ }
+
+ @FunctionalInterface
+ private interface ThrowingConsumer<S, T extends Throwable> {
+ void accept(S s) throws T;
+ }
+
+ private void invokeForAllStatsProviderCallbacks(
+ @NonNull ThrowingConsumer<NetworkStatsProviderCallbackImpl, RemoteException> task) {
+ for (final NetworkStatsProviderCallbackImpl cb : mStatsProviderCbList) {
+ try {
+ task.accept(cb);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Fail to broadcast to provider: " + cb.mTag, e);
+ }
+ }
+ }
+
+ private static class NetworkStatsProviderCallbackImpl extends INetworkStatsProviderCallback.Stub
+ implements IBinder.DeathRecipient {
+ @NonNull final String mTag;
+
+ @NonNull final INetworkStatsProvider mProvider;
+ @NonNull private final Semaphore mSemaphore;
+ @NonNull final INetworkManagementEventObserver mAlertObserver;
+ @NonNull final CopyOnWriteArrayList<NetworkStatsProviderCallbackImpl> mStatsProviderCbList;
+
+ @NonNull private final Object mProviderStatsLock = new Object();
+
+ @GuardedBy("mProviderStatsLock")
+ // Track STATS_PER_IFACE and STATS_PER_UID separately.
+ private final NetworkStats mIfaceStats = new NetworkStats(0L, 0);
+ @GuardedBy("mProviderStatsLock")
+ private final NetworkStats mUidStats = new NetworkStats(0L, 0);
+
+ NetworkStatsProviderCallbackImpl(
+ @NonNull String tag, @NonNull INetworkStatsProvider provider,
+ @NonNull Semaphore semaphore,
+ @NonNull INetworkManagementEventObserver alertObserver,
+ @NonNull CopyOnWriteArrayList<NetworkStatsProviderCallbackImpl> cbList)
+ throws RemoteException {
+ mTag = tag;
+ mProvider = provider;
+ mProvider.asBinder().linkToDeath(this, 0);
+ mSemaphore = semaphore;
+ mAlertObserver = alertObserver;
+ mStatsProviderCbList = cbList;
+ }
+
+ @NonNull
+ public NetworkStats getCachedStats(int how) {
+ synchronized (mProviderStatsLock) {
+ NetworkStats stats;
+ switch (how) {
+ case STATS_PER_IFACE:
+ stats = mIfaceStats;
+ break;
+ case STATS_PER_UID:
+ stats = mUidStats;
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid type: " + how);
+ }
+ // Callers might be able to mutate the returned object. Return a defensive copy
+ // instead of local reference.
+ return stats.clone();
+ }
+ }
+
+ @Override
+ public void notifyStatsUpdated(int token, @Nullable NetworkStats ifaceStats,
+ @Nullable NetworkStats uidStats) {
+ // TODO: 1. Use token to map ifaces to correct NetworkIdentity.
+ // 2. Store the difference and store it directly to the recorder.
+ synchronized (mProviderStatsLock) {
+ if (ifaceStats != null) mIfaceStats.combineAllValues(ifaceStats);
+ if (uidStats != null) mUidStats.combineAllValues(uidStats);
+ }
+ mSemaphore.release();
+ }
+
+ @Override
+ public void notifyAlertReached() throws RemoteException {
+ // This binder object can only have been obtained by a process that holds
+ // NETWORK_STATS_PROVIDER. Thus, no additional permission check is required.
+ BinderUtils.withCleanCallingIdentity(() ->
+ mAlertObserver.limitReached(LIMIT_GLOBAL_ALERT, null /* unused */));
+ }
+
+ @Override
+ public void notifyWarningOrLimitReached() {
+ Log.d(TAG, mTag + ": notifyWarningOrLimitReached");
+ BinderUtils.withCleanCallingIdentity(() ->
+ LocalServices.getService(NetworkPolicyManagerInternal.class)
+ .onStatsProviderWarningOrLimitReached(mTag));
+ }
+
+ @Override
+ public void binderDied() {
+ Log.d(TAG, mTag + ": binderDied");
+ mStatsProviderCbList.remove(this);
+ }
+
+ @Override
+ public void unregister() {
+ Log.d(TAG, mTag + ": unregister");
+ mStatsProviderCbList.remove(this);
+ }
+
+ }
+
+ private void assertSystemReady() {
+ if (!mSystemReady) {
+ throw new IllegalStateException("System not ready");
+ }
+ }
+
+ private class DropBoxNonMonotonicObserver implements NonMonotonicObserver<String> {
+ @Override
+ public void foundNonMonotonic(NetworkStats left, int leftIndex, NetworkStats right,
+ int rightIndex, String cookie) {
+ Log.w(TAG, "Found non-monotonic values; saving to dropbox");
+
+ // record error for debugging
+ final StringBuilder builder = new StringBuilder();
+ builder.append("found non-monotonic " + cookie + " values at left[" + leftIndex
+ + "] - right[" + rightIndex + "]\n");
+ builder.append("left=").append(left).append('\n');
+ builder.append("right=").append(right).append('\n');
+
+ mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR,
+ builder.toString());
+ }
+
+ @Override
+ public void foundNonMonotonic(
+ NetworkStats stats, int statsIndex, String cookie) {
+ Log.w(TAG, "Found non-monotonic values; saving to dropbox");
+
+ final StringBuilder builder = new StringBuilder();
+ builder.append("Found non-monotonic " + cookie + " values at [" + statsIndex + "]\n");
+ builder.append("stats=").append(stats).append('\n');
+
+ mContext.getSystemService(DropBoxManager.class).addText(TAG_NETSTATS_ERROR,
+ builder.toString());
+ }
+ }
+
+ /**
+ * Default external settings that read from
+ * {@link android.provider.Settings.Global}.
+ */
+ private static class DefaultNetworkStatsSettings implements NetworkStatsSettings {
+ private final ContentResolver mResolver;
+
+ public DefaultNetworkStatsSettings(Context context) {
+ mResolver = Objects.requireNonNull(context.getContentResolver());
+ // TODO: adjust these timings for production builds
+ }
+
+ private long getGlobalLong(String name, long def) {
+ return Settings.Global.getLong(mResolver, name, def);
+ }
+ private boolean getGlobalBoolean(String name, boolean def) {
+ final int defInt = def ? 1 : 0;
+ return Settings.Global.getInt(mResolver, name, defInt) != 0;
+ }
+
+ @Override
+ public long getPollInterval() {
+ return getGlobalLong(NETSTATS_POLL_INTERVAL, 30 * MINUTE_IN_MILLIS);
+ }
+ @Override
+ public long getPollDelay() {
+ return DEFAULT_PERFORM_POLL_DELAY_MS;
+ }
+ @Override
+ public long getGlobalAlertBytes(long def) {
+ return getGlobalLong(NETSTATS_GLOBAL_ALERT_BYTES, def);
+ }
+ @Override
+ public boolean getSampleEnabled() {
+ return getGlobalBoolean(NETSTATS_SAMPLE_ENABLED, true);
+ }
+ @Override
+ public boolean getAugmentEnabled() {
+ return getGlobalBoolean(NETSTATS_AUGMENT_ENABLED, true);
+ }
+ @Override
+ public boolean getCombineSubtypeEnabled() {
+ return getGlobalBoolean(NETSTATS_COMBINE_SUBTYPE_ENABLED, false);
+ }
+ @Override
+ public Config getDevConfig() {
+ return new Config(getGlobalLong(NETSTATS_DEV_BUCKET_DURATION, HOUR_IN_MILLIS),
+ getGlobalLong(NETSTATS_DEV_ROTATE_AGE, 15 * DAY_IN_MILLIS),
+ getGlobalLong(NETSTATS_DEV_DELETE_AGE, 90 * DAY_IN_MILLIS));
+ }
+ @Override
+ public Config getXtConfig() {
+ return getDevConfig();
+ }
+ @Override
+ public Config getUidConfig() {
+ return new Config(getGlobalLong(NETSTATS_UID_BUCKET_DURATION, 2 * HOUR_IN_MILLIS),
+ getGlobalLong(NETSTATS_UID_ROTATE_AGE, 15 * DAY_IN_MILLIS),
+ getGlobalLong(NETSTATS_UID_DELETE_AGE, 90 * DAY_IN_MILLIS));
+ }
+ @Override
+ public Config getUidTagConfig() {
+ return new Config(getGlobalLong(NETSTATS_UID_TAG_BUCKET_DURATION, 2 * HOUR_IN_MILLIS),
+ getGlobalLong(NETSTATS_UID_TAG_ROTATE_AGE, 5 * DAY_IN_MILLIS),
+ getGlobalLong(NETSTATS_UID_TAG_DELETE_AGE, 15 * DAY_IN_MILLIS));
+ }
+ @Override
+ public long getDevPersistBytes(long def) {
+ return getGlobalLong(NETSTATS_DEV_PERSIST_BYTES, def);
+ }
+ @Override
+ public long getXtPersistBytes(long def) {
+ return getDevPersistBytes(def);
+ }
+ @Override
+ public long getUidPersistBytes(long def) {
+ return getGlobalLong(NETSTATS_UID_PERSIST_BYTES, def);
+ }
+ @Override
+ public long getUidTagPersistBytes(long def) {
+ return getGlobalLong(NETSTATS_UID_TAG_PERSIST_BYTES, def);
+ }
+ }
+
+ private static native long nativeGetTotalStat(int type);
+ private static native long nativeGetIfaceStat(String iface, int type);
+ private static native long nativeGetUidStat(int uid, int type);
+}
diff --git a/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java b/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java
new file mode 100644
index 0000000..5646c75
--- /dev/null
+++ b/service-t/src/com/android/server/net/NetworkStatsSubscriptionsMonitor.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.net;
+
+import static android.net.NetworkTemplate.NETWORK_TYPE_5G_NSA;
+import static android.net.NetworkTemplate.getCollapsedRatType;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Looper;
+import android.telephony.Annotation;
+import android.telephony.NetworkRegistrationInfo;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+
+/**
+ * Helper class that watches for events that are triggered per subscription.
+ */
+public class NetworkStatsSubscriptionsMonitor extends
+ SubscriptionManager.OnSubscriptionsChangedListener {
+
+ /**
+ * Interface that this monitor uses to delegate event handling to NetworkStatsService.
+ */
+ public interface Delegate {
+ /**
+ * Notify that the collapsed RAT type has been changed for any subscription. The method
+ * will also be triggered for any existing sub when start and stop monitoring.
+ *
+ * @param subscriberId IMSI of the subscription.
+ * @param collapsedRatType collapsed RAT type.
+ * @see android.net.NetworkTemplate#getCollapsedRatType(int).
+ */
+ void onCollapsedRatTypeChanged(@NonNull String subscriberId,
+ @Annotation.NetworkType int collapsedRatType);
+ }
+ private final Delegate mDelegate;
+
+ /**
+ * Receivers that watches for {@link ServiceState} changes for each subscription, to
+ * monitor the transitioning between Radio Access Technology(RAT) types for each sub.
+ */
+ @NonNull
+ private final CopyOnWriteArrayList<RatTypeListener> mRatListeners =
+ new CopyOnWriteArrayList<>();
+
+ @NonNull
+ private final SubscriptionManager mSubscriptionManager;
+ @NonNull
+ private final TelephonyManager mTeleManager;
+
+ @NonNull
+ private final Executor mExecutor;
+
+ NetworkStatsSubscriptionsMonitor(@NonNull Context context, @NonNull Looper looper,
+ @NonNull Executor executor, @NonNull Delegate delegate) {
+ super(looper);
+ mSubscriptionManager = (SubscriptionManager) context.getSystemService(
+ Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+ mTeleManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ mExecutor = executor;
+ mDelegate = delegate;
+ }
+
+ @Override
+ public void onSubscriptionsChanged() {
+ // Collect active subId list, hidden subId such as opportunistic subscriptions are
+ // also needed to track CBRS.
+ final List<Integer> newSubs = getActiveSubIdList(mSubscriptionManager);
+
+ // IMSI is needed for every newly added sub. Listener stores subscriberId into it to
+ // prevent binder call to telephony when querying RAT. Keep listener registration with empty
+ // IMSI is meaningless since the RAT type changed is ambiguous for multi-SIM if reported
+ // with empty IMSI. So filter the subs w/o a valid IMSI to prevent such registration.
+ final List<Pair<Integer, String>> filteredNewSubs =
+ CollectionUtils.mapNotNull(newSubs, subId -> {
+ final String subscriberId = mTeleManager.getSubscriberId(subId);
+ return TextUtils.isEmpty(subscriberId) ? null : new Pair(subId, subscriberId);
+ });
+
+ for (final Pair<Integer, String> sub : filteredNewSubs) {
+ // Fully match listener with subId and IMSI, since in some rare cases, IMSI might be
+ // suddenly change regardless of subId, such as switch IMSI feature in modem side.
+ // If that happens, register new listener with new IMSI and remove old one later.
+ if (CollectionUtils.find(mRatListeners,
+ it -> it.equalsKey(sub.first, sub.second)) != null) {
+ continue;
+ }
+
+ final RatTypeListener listener =
+ new RatTypeListener(mExecutor, this, sub.first, sub.second);
+ mRatListeners.add(listener);
+
+ // Register listener to the telephony manager that associated with specific sub.
+ mTeleManager.createForSubscriptionId(sub.first)
+ .listen(listener, PhoneStateListener.LISTEN_SERVICE_STATE);
+ Log.d(NetworkStatsService.TAG, "RAT type listener registered for sub " + sub.first);
+ }
+
+ for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
+ // If there is no subId and IMSI matched the listener, removes it.
+ if (CollectionUtils.find(filteredNewSubs,
+ it -> listener.equalsKey(it.first, it.second)) == null) {
+ handleRemoveRatTypeListener(listener);
+ }
+ }
+ }
+
+ @NonNull
+ private List<Integer> getActiveSubIdList(@NonNull SubscriptionManager subscriptionManager) {
+ final ArrayList<Integer> ret = new ArrayList<>();
+ final int[] ids = subscriptionManager.getCompleteActiveSubscriptionIdList();
+ for (int id : ids) ret.add(id);
+ return ret;
+ }
+
+ /**
+ * Get a collapsed RatType for the given subscriberId.
+ *
+ * @param subscriberId the target subscriberId
+ * @return collapsed RatType for the given subscriberId
+ */
+ public int getRatTypeForSubscriberId(@NonNull String subscriberId) {
+ final RatTypeListener match = CollectionUtils.find(mRatListeners,
+ it -> TextUtils.equals(subscriberId, it.mSubscriberId));
+ return match != null ? match.mLastCollapsedRatType : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+ }
+
+ /**
+ * Start monitoring events that triggered per subscription.
+ */
+ public void start() {
+ mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor, this);
+ }
+
+ /**
+ * Unregister subscription changes and all listeners for each subscription.
+ */
+ public void stop() {
+ mSubscriptionManager.removeOnSubscriptionsChangedListener(this);
+
+ for (final RatTypeListener listener : new ArrayList<>(mRatListeners)) {
+ handleRemoveRatTypeListener(listener);
+ }
+ }
+
+ private void handleRemoveRatTypeListener(@NonNull RatTypeListener listener) {
+ mTeleManager.createForSubscriptionId(listener.mSubId)
+ .listen(listener, PhoneStateListener.LISTEN_NONE);
+ Log.d(NetworkStatsService.TAG, "RAT type listener unregistered for sub " + listener.mSubId);
+ mRatListeners.remove(listener);
+
+ // Removal of subscriptions doesn't generate RAT changed event, fire it for every
+ // RatTypeListener.
+ mDelegate.onCollapsedRatTypeChanged(
+ listener.mSubscriberId, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ }
+
+ static class RatTypeListener extends PhoneStateListener {
+ // Unique id for the subscription. See {@link SubscriptionInfo#getSubscriptionId}.
+ @NonNull
+ private final int mSubId;
+
+ // IMSI to identifying the corresponding network from {@link NetworkState}.
+ // See {@link TelephonyManager#getSubscriberId}.
+ @NonNull
+ private final String mSubscriberId;
+
+ private volatile int mLastCollapsedRatType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+ @NonNull
+ private final NetworkStatsSubscriptionsMonitor mMonitor;
+
+ RatTypeListener(@NonNull Executor executor,
+ @NonNull NetworkStatsSubscriptionsMonitor monitor, int subId,
+ @NonNull String subscriberId) {
+ super(executor);
+ mSubId = subId;
+ mSubscriberId = subscriberId;
+ mMonitor = monitor;
+ }
+
+ @Override
+ public void onServiceStateChanged(@NonNull ServiceState ss) {
+ // In 5G SA (Stand Alone) mode, the primary cell itself will be 5G hence telephony
+ // would report RAT = 5G_NR.
+ // However, in 5G NSA (Non Stand Alone) mode, the primary cell is still LTE and
+ // network allocates a secondary 5G cell so telephony reports RAT = LTE along with
+ // NR state as connected. In such case, attributes the data usage to NR.
+ // See b/160727498.
+ final boolean is5GNsa = (ss.getDataNetworkType() == TelephonyManager.NETWORK_TYPE_LTE
+ || ss.getDataNetworkType() == TelephonyManager.NETWORK_TYPE_LTE_CA)
+ && ss.getNrState() == NetworkRegistrationInfo.NR_STATE_CONNECTED;
+
+ final int networkType =
+ (is5GNsa ? NETWORK_TYPE_5G_NSA : ss.getDataNetworkType());
+ final int collapsedRatType = getCollapsedRatType(networkType);
+ if (collapsedRatType == mLastCollapsedRatType) return;
+
+ if (NetworkStatsService.LOGD) {
+ Log.d(NetworkStatsService.TAG, "subtype changed for sub(" + mSubId + "): "
+ + mLastCollapsedRatType + " -> " + collapsedRatType);
+ }
+ mLastCollapsedRatType = collapsedRatType;
+ mMonitor.mDelegate.onCollapsedRatTypeChanged(mSubscriberId, mLastCollapsedRatType);
+ }
+
+ @VisibleForTesting
+ public int getSubId() {
+ return mSubId;
+ }
+
+ boolean equalsKey(int subId, @NonNull String subscriberId) {
+ return mSubId == subId && TextUtils.equals(mSubscriberId, subscriberId);
+ }
+ }
+}